# مستندات API فروش اشتراک آپموی

نسخهٔ ۱ · آدرس پایه: `https://api.upmovi.com/v1` (آدرس معادل: `https://api.upmovi.ir/v1`)

این API برای سایت‌های همکار (مثل سایت شارژ) ساخته شده تا بتوانند **اشتراک سایت فیلم را بفروشند** و **کاربر را روی سایت فیلم ثبت‌نام کنند**. پرداخت کاربر در سایت شما انجام می‌شود؛ بعد از پرداخت موفق، یک درخواست به این API می‌فرستید تا اشتراک روی شمارهٔ موبایل او فعال شود.

## جریان پیشنهادی

1. کاربر در سایت شما وارد پنل می‌شود و صفحهٔ پلن‌ها را باز می‌کند.
2. سایت شما `GET /plans` را صدا می‌زند و پلن‌ها و قیمت‌های روز را نشان می‌دهد (دکمهٔ «به‌روزرسانی» هم همین را دوباره می‌خواند).
3. کاربر پلن را انتخاب می‌کند، **یک `operation_key` (UUID) می‌سازید و در دیتابیس خودتان ذخیره می‌کنید**، و پرداخت انجام می‌شود.
4. اگر پرداخت موفق بود: `POST /orders` با همان `operation_key`.
5. در پاسخ، تاریخ پایان اشتراک و (اگر حساب تازه ساخته شده) **رمز عبور** برمی‌گردد. رمز فقط همین یک‌بار نمایش داده می‌شود؛ آن را به کاربر نشان دهید یا پیامک کنید.
6. اگر درخواست قطع شد یا پاسخی نگرفتید، همان درخواست را با **همان `operation_key`** دوباره بفرستید (امن است) یا `GET /orders/{operation_key}` را بخوانید.

## احراز هویت

همهٔ درخواست‌ها باید این هدر را داشته باشند:

```
Authorization: Bearer <API_KEY>
Accept: application/json
```

- کلید را مدیر سایت فیلم (آپموی) برای شما می‌سازد و فقط یک‌بار به شما نشان داده می‌شود (روی سرور فقط هش آن ذخیره می‌شود).
- کلید را فقط در سمت سرور (بک‌اند) سایت خود نگه دارید؛ هرگز در جاوااسکریپت صفحه یا اپلیکیشن قرار ندهید.
- می‌توان کلید را به IPهای مشخص محدود کرد (لیست IP مجاز). درخواست از IP دیگر با خطای `403 ip_not_allowed` رد می‌شود.
- اگر کلید لو رفت، از مدیر سایت فیلم کلید جدید بخواهید؛ کلید قبلی همان لحظه از کار می‌افتد.

## قواعد کلی

| مورد | توضیح |
|---|---|
| قالب | ورودی و خروجی JSON؛ برای `POST` هدر `Content-Type: application/json` بفرستید |
| پول | تومان، عدد صحیح (`IRT`) |
| زمان | UTC به قالب `2026-12-15T23:59:59Z` |
| موبایل | `09123456789`؛ ارقام فارسی، `+98...` و `98...` هم پذیرفته و نرمال می‌شود |
| محدودیت | ۱۲۰ درخواست در دقیقه برای هر کلید؛ ۲۰ تلاش ناموفق در دقیقه برای هر IP |
| کش | پاسخ‌ها `no-store` هستند |

پاسخ موفق: `{"ok": true, "data": {...}}` — پاسخ خطا: `{"ok": false, "error": {"code": "...", "message": "..."}}`

## ۱) فهرست پلن‌ها — `GET /plans`

قیمت‌ها همیشه از خود سایت فیلم خوانده می‌شوند (با تخفیف فعال). پیشنهاد: هنگام نمایش به کاربر دوباره بخوانید یا حداکثر چند دقیقه کش کنید.

```json
{
  "ok": true,
  "data": {
    "currency": "IRT", "unit": "toman",
    "version": "a1b2c3d4e5f60789",
    "generated_at": "2026-11-15T06:00:00Z",
    "plans": [
      {
        "id": 10, "name": "1-Month Subscription", "name_fa": "اشتراک 1 ماهه",
        "duration_days": 30,
        "original_price": 272000, "final_price": 190000, "discount_percent": 30,
        "discount_until": null, "is_popular": false, "badge": null, "features": null
      }
    ]
  }
}
```

- `final_price` مبلغی است که باید از کاربر بگیرید. `original_price` قیمت خط‌خورده است.
- `version` فقط وقتی قیمت یا پلنی تغییر کند عوض می‌شود؛ برای تشخیص تغییر مناسب است.

## ۲) بررسی حساب — `GET /accounts?mobile=09...`

```json
{ "ok": true, "data": { "exists": true, "mobile": "09123456789", "user_id": 123,
  "status": "active", "has_password": true, "active_until": "2026-12-15T23:59:59Z" } }
```

اگر حساب نباشد: `{"exists": false, "mobile": "..."}`. برای نمایش پیام «اشتراک فعلی شما تا ... است» یا «حساب جدید ساخته می‌شود» مفید است.

## ۳) ثبت سفارش پرداخت‌شده — `POST /orders`

فقط **بعد از پرداخت موفق** صدا بزنید.

| فیلد | الزامی | توضیح |
|---|---|---|
| `operation_key` | بله | UUID یکتا برای هر خرید (همان را در تکرار هم بفرستید) |
| `mobile` | بله | موبایل خریدار |
| `plan_id` | بله | شناسهٔ پلن از `/plans` |
| `amount` | بله | تومانی که کاربر پرداخته؛ نباید کمتر از `final_price` فعلی پلن باشد (وگرنه `409 amount_too_low`) |
| `reference` | خیر | کد پیگیری پرداخت در سایت شما (برای گزارش) |
| `password` | خیر | فقط برای حساب **تازه**: اگر بفرستید همان رمز ثبت می‌شود، وگرنه رمز تصادفی ۱۰ حرفی ساخته و برگردانده می‌شود |

```json
{
  "operation_key": "5f7480c7-1be9-4f13-9f6a-69633840dab5",
  "mobile": "09123456789", "plan_id": 10, "amount": 190000, "reference": "TRK-889123"
}
```

پاسخ موفق (`201` بار اول):

```json
{ "ok": true, "data": {
  "operation_key": "5f7480c7-1be9-4f13-9f6a-69633840dab5",
  "already_applied": false, "order_id": 3000000257,
  "user": { "id": 3000000555, "mobile": "09123456789", "created": true, "has_password": true },
  "subscription": { "plan_id": 10, "end_date": "2026-12-15T23:59:59Z" },
  "password": "k7Hq2mXpWc"
} }
```

- `user.created = true` یعنی حساب تازه ساخته شد و فیلد `password` فقط همین‌جا هست.
- اگر موبایل از قبل حساب داشته باشد، اشتراک به همان حساب اضافه/تمدید می‌شود، رمز تغییر نمی‌کند و `password` برنمی‌گردد (کاربر با رمز قبلی یا «فراموشی رمز» وارد شود).
- تکرار با همان `operation_key` و همان مشخصات: `200` با `already_applied: true` (اشتراک دوباره داده نمی‌شود، رمز برنمی‌گردد).
- همان `operation_key` با موبایل/پلن دیگر: `409 order_rejected`.
- حساب غیرفعال/مسدود: `403 account_not_active`.

## ۴) وضعیت سفارش — `GET /orders/{operation_key}`

```json
{ "ok": true, "data": { "operation_key": "...", "order_id": 3000000257, "user_id": 3000000555,
  "plan_id": 10, "status": "paid", "amount": 190000, "applied": true, "end_date": "2026-12-15T23:59:59Z" } }
```

اگر سفارش نبود `404 not_found`. برای بازبینی بعد از timeout یا خطای ۵۰۰ استفاده کنید.

## خطاها

| HTTP | code | معنی |
|---|---|---|
| 401 | unauthorized | کلید نیست یا اشتباه است |
| 403 | ip_not_allowed | IP سرور شما در لیست مجاز نیست |
| 403 | account_not_active | حساب این موبایل فعال نیست |
| 404 | plan_not_found | پلن وجود ندارد یا برای فروش نیست |
| 404 | not_found | سفارشی با این operation_key نیست |
| 409 | amount_too_low | مبلغ کمتر از قیمت فعلی است؛ دوباره `/plans` را بخوانید |
| 409 | order_rejected | operation_key برای درخواست دیگری استفاده شده یا سفارش قبلی ناقص است |
| 422 | validation_failed / invalid_mobile / invalid_operation_key | ورودی نامعتبر |
| 429 | rate_limited / too_many_requests | درخواست زیاد؛ کمی صبر کنید |
| 500 | server_error | خطای سرور؛ با همان operation_key دوباره بفرستید یا وضعیت را بخوانید |

## نکات مهم برای پیاده‌سازی

- **Idempotency:** `operation_key` را قبل از پرداخت بسازید و ذخیره کنید؛ در هر تکرار همان را بفرستید. هیچ‌وقت برای هر تلاش یک UUID جدید نسازید، وگرنه ممکن است اشتراک دوبار ثبت شود.
- اگر `POST /orders` بعد از پرداخت موفق نشد (timeout/۵۰۰)، سفارش را «در انتظار تأیید» نگه دارید و با تلاش مجدد تکمیل کنید؛ پول کاربر نباید بی‌اشتراک بماند.
- رمز تولیدشده را در لاگ ننویسید و بعد از نمایش/پیامک، دور بریزید.
- شمارهٔ موبایل خریدار همان شمارهٔ ورود او در سایت فیلم است.

## نمونه کد

### curl

```bash
curl -s https://api.upmovi.com/v1/plans \
  -H "Authorization: Bearer $API_KEY" -H "Accept: application/json"

curl -s -X POST https://api.upmovi.com/v1/orders \
  -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
  -d '{"operation_key":"5f7480c7-1be9-4f13-9f6a-69633840dab5","mobile":"09123456789","plan_id":10,"amount":190000,"reference":"TRK-889123"}'
```

### PHP

```php
function upmoviApi(string $method, string $path, ?array $body = null): array
{
    $ch = curl_init('https://api.upmovi.com/v1' . $path);
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST  => $method,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 25,
        CURLOPT_HTTPHEADER     => [
            'Authorization: Bearer ' . getenv('UPMOVI_API_KEY'),
            'Accept: application/json',
            'Content-Type: application/json',
        ],
        CURLOPT_POSTFIELDS     => $body === null ? null : json_encode($body),
    ]);
    $raw = curl_exec($ch);
    $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    return ['status' => $code, 'json' => json_decode((string) $raw, true)];
}

$plans = upmoviApi('GET', '/plans');
$order = upmoviApi('POST', '/orders', [
    'operation_key' => $savedUuid, 'mobile' => $mobile,
    'plan_id' => 10, 'amount' => 190000, 'reference' => $trackingCode,
]);
if (in_array($order['status'], [200, 201], true)) {
    $password = $order['json']['data']['password'] ?? null; // فقط برای حساب تازه
}
```

## چک‌لیست راه‌اندازی

- [ ] کلید ساخته شد و فقط در سرور نگه‌داری می‌شود
- [ ] IP سرور شما در «لیست IP مجاز» کلید ثبت شد
- [ ] `operation_key` قبل از پرداخت ساخته و ذخیره می‌شود
- [ ] مبلغ ارسالی `amount` همان `final_price` لحظهٔ نمایش (یا بیشتر) است
- [ ] رمز بازگشتی فقط یک‌بار به کاربر نشان داده می‌شود
- [ ] تکرار خودکار برای خطای ۵۰۰/timeout با همان `operation_key` پیاده شد