# Birinchishop Partner API — guide for AI assistants

> Give this whole file to Claude, Cursor, ChatGPT or another AI as context and ask it to write the integration. Generated from the live OpenAPI spec (version `2026-10-23`). Human docs: https://birinchishop.uz/api/v1/docs · machine spec: https://birinchishop.uz/api/v1/openapi.json
>
> **O'zbekcha:** bu fayl AI yordamchi uchun. Uni AI ga yuklang va pastdagi tayyor promptlardan birini yozing — integratsiya kodini AI yozib beradi. API kalitini faylga, sayt yoki ilova kodiga YOZMANG — faqat serveringizdagi `.env` da saqlang.

## Rules for the AI writing the code
- The API key is a server-side secret. Never put it in browser JavaScript, a mobile app, a public repo or a URL. Read it from an environment variable (e.g. `BSK_KEY`).
- `POST /orders` creates a REAL order: a call center phones the customer. Never send test, sample or fake orders — the account gets blocked. Always send an `Idempotency-Key` header so retries never duplicate.
- Branch on `error.code`, never on `error.message` (messages may change).
- Money is integer UZS. Date filters are Asia/Tashkent (UTC+5); timestamps are ISO 8601 UTC.
- Customer phone numbers in responses are always masked.
- On `429` wait for the `Retry-After` / `RateLimit-Reset` header, then retry. Retry `5xx` with exponential backoff.
- Prefer `GET /events` (cursor) or webhooks over re-reading all orders.

## How the business works
- Customers pay on delivery (cash on delivery). A delivery fee (typically 30,000 UZS) is added to customer_price by the delivery partner.
- Payout is credited only when an order reaches "delivered"; if the status is later reverted, the payout is reversed.
- One phone number cannot order the same offer twice within 24 hours, and at most 3 different offers per 24 hours.
- Customer phone numbers are always masked for partners.
- Submitting orders via API (orders:write) requires at least 3 delivered orders on the account, or admin approval.
- Fake or test orders are forbidden and lead to account blocking.
- sale_rate_percent (share of finished orders that get delivered) matters more than payout: 30 000 UZS at 40% beats 70 000 UZS at 5%.
- sale_rate_percent = delivered / finished orders over the last 60 days across the network (null if not enough data). sale_rate_tier: good (>=25%), normal, low (<10%).

## Authentication
Base URL: `https://birinchishop.uz/api/v1`
```http
Authorization: Bearer bsk_live_<id>_<secret>
```
- REST and MCP: header "Authorization: Bearer bsk_live_<id>_<secret>" (or X-API-Key). Keys are created in the partner panel (Dashboard -> API) with chosen scopes, optional IP allowlist and expiry.
- MCP only (Claude.ai / ChatGPT connectors): OAuth 2.1 authorization code + PKCE S256. Metadata: https://birinchishop.uz/.well-known/oauth-protected-resource/api/v1/mcp and https://birinchishop.uz/.well-known/oauth-authorization-server. Access token 1 hour, rotating refresh tokens, connection valid up to 180 days, revocable in the panel.

| Scope | Allows |
|---|---|
| `account:read` | Read profile and balance |
| `offers:read` | Read the offers catalog |
| `links:read` | Read tracking links |
| `links:write` | Create tracking links |
| `orders:read` | Read orders and their statuses |
| `orders:write` | Submit orders (leads) |
| `stats:read` | Read statistics |
| `events:read` | Read the event feed (order status changes) |
| `webhooks:manage` | Manage webhook/postback endpoints |
| `insights:read` | Read AI manager recommendations and order sale chance |
| `ads:read` | Read Meta ad campaigns and their results |
| `ads:write` | Pause ad campaigns; resume/budget changes require the partner's confirmation |

## Response format
```json
{ "data": { }, "meta": { "page": 1, "limit": 20, "total": 57 }, "request_id": "req_..." }
```
Errors:
```json
{ "error": { "code": "duplicate_order", "message": "…", "details": { } }, "request_id": "req_..." }
```

## Endpoints

### GET /me
Account, balance and key info — scope `account:read`

Response `200`: `data` is `Account` — fields in **Objects** below.

Possible HTTP errors: 401, 403, 429

### GET /offers
Search active offers — scope `offers:read`

| Parameter | In | Type | Required | Notes |
|---|---|---|---|---|
| `search` | query | string |  | Search in product name |
| `sort` | query | `recommended` \| `newest` \| `best_selling` \| `highest_payout` \| `highest_stock` \| `highest_lead_bonus` |  | Sort order; default: "recommended" |
| `in_stock_only` | query | boolean |  | Only offers with stock > 0; default: true |
| `min_sale_rate` | query | integer |  | Minimum sale_rate_percent (0-100); min 0; max 100 |
| `min_payout` | query | integer |  | Minimum payout, UZS; min 0 |
| `ids` | query | string |  | Comma-separated offer ids (max 100) |
| `page` | query | integer |  | Page number (from 1); default: 1; min 1 |
| `limit` | query | integer |  | Items per page; default: 20; min 1; max 100 |

Response `200`: `data` is an array of `Offer` (paginated, see `meta`) — fields in **Objects** below.

Possible HTTP errors: 400, 401, 403, 429

### GET /offers/{offer_id}
Offer details with variants and your links — scope `offers:read`

| Parameter | In | Type | Required | Notes |
|---|---|---|---|---|
| `offer_id` | path | integer | yes | Offer id; min 1 |

Response `200`: `data` is `OfferDetail` — fields in **Objects** below.

Possible HTTP errors: 401, 403, 404, 429

### GET /links
List your tracking links — scope `links:read`

| Parameter | In | Type | Required | Notes |
|---|---|---|---|---|
| `offer_id` | query | integer |  | Only links for this offer; min 1 |
| `page` | query | integer |  | Page number (from 1); default: 1; min 1 |
| `limit` | query | integer |  | Items per page; default: 20; min 1; max 100 |

Response `200`: `data` is an array of `Link` (paginated, see `meta`) — fields in **Objects** below.

Possible HTTP errors: 400, 401, 403, 429

### POST /links
Create a tracking link — scope `links:write`

Request body (JSON):
| Field | Type | Required | Notes |
|---|---|---|---|
| `offer_id` | integer | yes | min 1 |
| `name` | string |  | max 100 chars |

Response `201`: `data` is `Link` — fields in **Objects** below.

Possible HTTP errors: 400, 401, 403, 404, 409, 429

### GET /links/{link_id}
One tracking link — scope `links:read`

| Parameter | In | Type | Required | Notes |
|---|---|---|---|---|
| `link_id` | path | integer | yes | Link id; min 1 |

Response `200`: `data` is `Link` — fields in **Objects** below.

Possible HTTP errors: 401, 403, 404, 429

### GET /orders
List your orders — scope `orders:read`

| Parameter | In | Type | Required | Notes |
|---|---|---|---|---|
| `status` | query | `new` \| `processing` \| `callback` \| `confirmed` \| `packed` \| `shipping` \| `delivered` \| `returned` \| `cancelled` |  | Order status |
| `conversion_status` | query | `lead` \| `approved` \| `sale` \| `rejected` |  | Status group |
| `date_from` | query | string |  | Created on/after (Asia/Tashkent), YYYY-MM-DD |
| `date_to` | query | string |  | Created on/before (Asia/Tashkent), YYYY-MM-DD |
| `updated_since` | query | string |  | ISO 8601 datetime. Returns orders updated after it, ordered by updated_at ascending (incremental sync) |
| `link_id` | query | integer |  | Filter by link; min 1 |
| `offer_id` | query | integer |  | Filter by offer; min 1 |
| `click_id` | query | string |  | Filter by tracker click id |
| `external_id` | query | string |  | Filter by your external id |
| `page` | query | integer |  | Page number (from 1); default: 1; min 1 |
| `limit` | query | integer |  | Items per page; default: 20; min 1; max 100 |

Response `200`: `data` is an array of `Order` (paginated, see `meta`) — fields in **Objects** below.

Possible HTTP errors: 400, 401, 403, 429

### POST /orders
Submit a real customer order (lead) — scope `orders:write`

Creates a REAL order; a call center will phone the customer. Never send test or fake orders. Send an `Idempotency-Key` header so that retries never create duplicates (a replay returns 200 with header `Idempotent-Replayed: true`).

| Parameter | In | Type | Required | Notes |
|---|---|---|---|---|
| `Idempotency-Key` | header | string |  |  |

Request body (JSON):
| Field | Type | Required | Notes |
|---|---|---|---|
| `link_id` | integer | yes | Your link; determines the offer; min 1 |
| `phone` | string | yes | Uzbekistan mobile: +998XXXXXXXXX (other common formats are normalized); max 20 chars |
| `name` | string |  | Customer name (default "Mijoz <last 4 digits>"); max 100 chars |
| `region` | string |  | max 64 chars |
| `variant_id` | integer |  | Defaults to the first in-stock variant; min 1 |
| `variant_type_id` | integer |  | min 1 |
| `external_id` | string |  | Your own id for this order; max 128 chars |
| `click_id` | string |  | max 255 chars |
| `sub1` | string |  | max 255 chars |
| `sub2` | string |  | max 255 chars |
| `sub3` | string |  | max 255 chars |
| `sub4` | string |  | max 255 chars |
| `sub5` | string |  | max 255 chars |

Response `201`: `data` is `Order` — fields in **Objects** below.

Possible HTTP errors: 400, 401, 403, 404, 409, 410, 415, 429, 503

### GET /orders/{order_id}
One order — scope `orders:read`

| Parameter | In | Type | Required | Notes |
|---|---|---|---|---|
| `order_id` | path | integer | yes | Order id; min 1 |

Response `200`: `data` is `Order` — fields in **Objects** below.

Possible HTTP errors: 401, 403, 404, 429

### GET /stats
Aggregated statistics — scope `stats:read`

| Parameter | In | Type | Required | Notes |
|---|---|---|---|---|
| `group_by` | query | `day` \| `offer` \| `link` |  | Grouping; default: "day" |
| `date_from` | query | string |  | YYYY-MM-DD (Asia/Tashkent). Default: 29 days before date_to |
| `date_to` | query | string |  | YYYY-MM-DD (Asia/Tashkent). Default: today |
| `link_id` | query | integer |  | Filter by link; min 1 |
| `offer_id` | query | integer |  | Filter by offer; min 1 |

Response `200`: `data` is an array of `StatsRow` (paginated, see `meta`) — fields in **Objects** below.

Possible HTTP errors: 400, 401, 403, 429

### GET /events
Order events feed (cursor-based) — scope `events:read`

Poll this instead of (or in addition to) webhooks. Store meta.next_cursor and pass it as `after` next time. Events are kept for 90 days.

| Parameter | In | Type | Required | Notes |
|---|---|---|---|---|
| `after` | query | integer |  | Cursor: return events with id greater than this; default: 0; min 0 |
| `type` | query | `order.created` \| `order.status_changed` |  | Event type |
| `include_order` | query | boolean |  | Embed the current order; default: false |
| `limit` | query | integer |  | Max events; default: 100; min 1; max 200 |

Response `200`: `data` is an array of `Event` (paginated, see `meta`) — fields in **Objects** below.

Possible HTTP errors: 400, 401, 403, 429

### GET /orders/{order_id}/sale-chance
Probability that the order gets delivered — scope `insights:read`

Estimate 0-100% from the offer sale rate, the order stage, the customer's previous orders across the network, duplicates and rejected leads. The latest value is also in Order.sale_chance_percent.

| Parameter | In | Type | Required | Notes |
|---|---|---|---|---|
| `order_id` | path | integer | yes | Order id; min 1 |

Response `200`: `data` is `SaleChance` — fields in **Objects** below.

Possible HTTP errors: 401, 403, 404, 429

### GET /insights
AI manager: today's overview and recommendations — scope `insights:read`

Yesterday's results, last 7 days vs previous week, promoted offers with sale rates and stock, Meta campaigns, suggested offers and prioritized recommendations (type + hint). Also the morning brief text.

Response `200`: `data` is `Insights` — fields in **Objects** below.

Possible HTTP errors: 401, 403, 429

### GET /ads/status
Meta ad account connection — scope `ads:read`

Response `200`: `data` is `object`.

Possible HTTP errors: 401, 403, 429

### GET /ads/campaigns
Meta campaigns with results — scope `ads:read`

| Parameter | In | Type | Required | Notes |
|---|---|---|---|---|
| `period` | query | `7d` \| `30d` \| `90d` |  | Stats period; default: "7d" |
| `include_archived` | query | boolean |  | default: false |

Response `200`: `data` is an array of `Campaign` (paginated, see `meta`) — fields in **Objects** below.

Possible HTTP errors: 400, 401, 403, 409, 429

### POST /ads/campaigns/{campaign_id}/pause
Pause a campaign (immediate) — scope `ads:write`

| Parameter | In | Type | Required | Notes |
|---|---|---|---|---|
| `campaign_id` | path | integer | yes | Campaign id; min 1 |

Response `200`: `data` is `object`.

| Field | Type | Required | Notes |
|---|---|---|---|
| `id` | integer |  |  |
| `status` | "paused" |  |  |
| `changed` | boolean |  |  |

Possible HTTP errors: 401, 403, 404, 409, 429

### POST /ads/campaigns/{campaign_id}/resume
Request to resume (needs partner confirmation) — scope `ads:write`

| Parameter | In | Type | Required | Notes |
|---|---|---|---|---|
| `campaign_id` | path | integer | yes | Campaign id; min 1 |

Response `202`: `data` is `Action` — fields in **Objects** below.

Possible HTTP errors: 401, 403, 404, 409, 429

### POST /ads/campaigns/{campaign_id}/budget
Request a daily budget change (needs partner confirmation) — scope `ads:write`

| Parameter | In | Type | Required | Notes |
|---|---|---|---|---|
| `campaign_id` | path | integer | yes | Campaign id; min 1 |

Request body (JSON):
| Field | Type | Required | Notes |
|---|---|---|---|
| `daily_budget` | number | yes |  |
| `unit` | `UZS` \| `ACCOUNT` |  | ACCOUNT = the ad account currency (often USD); default: "UZS" |

Response `202`: `data` is `Action` — fields in **Objects** below.

Possible HTTP errors: 400, 401, 403, 404, 409, 415, 429

### GET /actions/{action_id}
Status of a confirmation request — scope `ads:read`

| Parameter | In | Type | Required | Notes |
|---|---|---|---|---|
| `action_id` | path | integer | yes | Action id; min 1 |

Response `200`: `data` is `Action` — fields in **Objects** below.

Possible HTTP errors: 401, 403, 404, 429

### GET /webhooks
List webhooks/postbacks — scope `webhooks:manage`

Response `200`: `data` is an array of `Webhook` (paginated, see `meta`) — fields in **Objects** below.

Possible HTTP errors: 401, 403, 429

### POST /webhooks
Add a webhook or S2S postback — scope `webhooks:manage`

kind=json: we POST a signed JSON event. kind=postback: we GET your URL after replacing macros like {click_id}, {status}, {conversion_status}, {payout}. Only public http(s) URLs on ports 80/443/8080/8443. The signing secret is returned ONCE in this response.

Request body (JSON):
| Field | Type | Required | Notes |
|---|---|---|---|
| `kind` | `json` \| `postback` |  | default: "json" |
| `url` | string | yes | max 2000 chars |
| `events` | `order.created` \| `order.status_changed`[] |  | default: ["order.created","order.status_changed"] |
| `description` | string |  | max 120 chars |

Response `201`: `data` is `object`.

Possible HTTP errors: 400, 401, 403, 409, 429

### PATCH /webhooks/{webhook_id}
Update / re-enable a webhook — scope `webhooks:manage`

| Parameter | In | Type | Required | Notes |
|---|---|---|---|---|
| `webhook_id` | path | integer | yes | Webhook id; min 1 |

Request body (JSON):
| Field | Type | Required | Notes |
|---|---|---|---|
| `url` | string |  |  |
| `events` | `order.created` \| `order.status_changed`[] |  |  |
| `description` | string |  | max 120 chars |
| `is_active` | boolean |  |  |

Response `200`: `data` is `Webhook` — fields in **Objects** below.

Possible HTTP errors: 400, 401, 403, 404, 429

### DELETE /webhooks/{webhook_id}
Delete a webhook — scope `webhooks:manage`

| Parameter | In | Type | Required | Notes |
|---|---|---|---|---|
| `webhook_id` | path | integer | yes | Webhook id; min 1 |

Response `200`: `data` is `object`.

Possible HTTP errors: 401, 403, 404, 429

### POST /webhooks/{webhook_id}/test
Send a sample event now — scope `webhooks:manage`

| Parameter | In | Type | Required | Notes |
|---|---|---|---|---|
| `webhook_id` | path | integer | yes | Webhook id; min 1 |

Response `200`: `data` is `object`.

Possible HTTP errors: 401, 403, 404, 429

### GET /webhooks/{webhook_id}/deliveries
Recent delivery attempts — scope `webhooks:manage`

| Parameter | In | Type | Required | Notes |
|---|---|---|---|---|
| `webhook_id` | path | integer | yes | Webhook id; min 1 |
| `limit` | query | integer |  | Max rows; default: 50; min 1; max 100 |

Response `200`: `data` is an array of `object` (paginated, see `meta`).

Possible HTTP errors: 401, 403, 404, 429

### POST /mcp
MCP endpoint for AI agents (JSON-RPC 2.0) — no key needed

Model Context Protocol, Streamable HTTP transport, stateless (JSON responses, no SSE). Tools are filtered by the key scopes.

Request body (JSON):
_(no fields documented)_

### GET /reference
Enums, scopes, statuses and business rules — no key needed

Response `200`: `data` is `object`.

## Objects

### Error
| Field | Type | Required | Notes |
|---|---|---|---|
| `error` | object | yes |  |
| `error.code` | string | yes |  |
| `error.message` | string | yes |  |
| `error.details` | object[] |  |  |
| `request_id` | string | yes |  |

### PageMeta
| Field | Type | Required | Notes |
|---|---|---|---|
| `page` | integer |  |  |
| `limit` | integer |  |  |
| `total` | integer |  |  |
| `total_pages` | integer |  |  |

### Account
| Field | Type | Required | Notes |
|---|---|---|---|
| `id` | integer |  |  |
| `name` | string |  |  |
| `status` | string |  |  |
| `balance` | integer |  | Withdrawable balance, UZS |
| `balance_frozen` | integer |  | Amount in pending withdrawals, UZS |
| `min_withdrawal` | integer |  |  |
| `currency` | "UZS" |  |  |
| `created_at` | string |  | format: date-time |
| `api_key` | object |  |  |
| `api_key.name` | string |  |  |
| `api_key.scopes` | string[] |  |  |
| `api_key.expires_at` | string \| null |  | format: date-time |

### Offer
| Field | Type | Required | Notes |
|---|---|---|---|
| `id` | integer |  |  |
| `name` | string |  |  |
| `customer_price` | integer |  | Price the end customer pays (UZS), excluding the delivery fee |
| `customer_price_is_minimum` | boolean |  | True if variants have different prices and this is the lowest |
| `payout` | integer |  | Partner earnings per DELIVERED order (UZS) |
| `payout_is_minimum` | boolean |  |  |
| `lead_bonus` | integer |  | Extra per accepted lead (UZS), 0 if none |
| `currency` | "UZS" |  |  |
| `in_stock` | boolean |  |  |
| `stock` | integer |  |  |
| `sale_rate_percent` | integer \| null |  | Delivered / finished orders, last 60 days. null = not enough data |
| `sale_rate_tier` | `good` \| `normal` \| `low` \| `null` |  |  |
| `payment_type` | string |  | after_delivery = cash on delivery |
| `image_url` | string \| null |  |  |
| `video_url` | string \| null |  |  |
| `has_variants` | boolean |  |  |
| `created_at` | string |  | format: date-time |

### OfferDetail
_(no fields documented)_

### Link
| Field | Type | Required | Notes |
|---|---|---|---|
| `id` | integer |  |  |
| `name` | string |  |  |
| `offer_id` | integer |  |  |
| `offer_name` | string \| null |  |  |
| `offer_active` | boolean \| null |  |  |
| `url` | string |  | Public landing page with the order form. Use in ads |
| `tracking_url_template` | string |  | url with click_id and sub1..sub5 placeholders; values are stored on the order and returned in postbacks |
| `clicks` | integer |  |  |
| `orders` | integer |  |  |
| `delivered` | integer |  |  |
| `created_at` | string |  | format: date-time |

### Order
| Field | Type | Required | Notes |
|---|---|---|---|
| `id` | integer |  |  |
| `status` | `new` \| `processing` \| `callback` \| `confirmed` \| `packed` \| `shipping` \| `delivered` \| `returned` \| `cancelled` |  |  |
| `conversion_status` | `lead` \| `approved` \| `sale` \| `rejected` |  |  |
| `created_at` | string |  | format: date-time |
| `updated_at` | string |  | format: date-time |
| `offer` | object |  |  |
| `offer.id` | integer |  |  |
| `offer.name` | string \| null |  |  |
| `link` | object \| null |  |  |
| `link.id` | integer |  |  |
| `link.name` | string \| null |  |  |
| `variant` | object \| null |  |  |
| `variant.id` | integer |  |  |
| `variant.name` | string \| null |  |  |
| `variant.type_id` | integer \| null |  |  |
| `variant.type_name` | string \| null |  |  |
| `customer` | object |  |  |
| `customer.name` | string |  |  |
| `customer.phone_masked` | string |  |  |
| `customer.region` | string \| null |  |  |
| `customer_price` | integer |  |  |
| `payout` | integer |  |  |
| `payout_status` | `pending` \| `earned` \| `under_review` \| `none` |  |  |
| `currency` | "UZS" |  |  |
| `delivery_date` | string \| null |  | Delivery day agreed with the customer (open orders only); format: date |
| `operator_note` | string \| null |  | Latest call-center note (phones masked) |
| `courier_note` | string \| null |  | Latest courier note (phones masked) |
| `source` | `api` \| `panel` \| `link` \| `landing` \| `meta_ads` |  |  |
| `sale_chance_percent` | integer \| null |  | Latest estimated probability (0-100) that the order gets delivered; null if not computed yet. See /orders/{order_id}/sale-chance |
| `tracking` | object \| null |  |  |
| `tracking.click_id` | string \| null |  |  |
| `tracking.sub1` | string \| null |  |  |
| `tracking.sub2` | string \| null |  |  |
| `tracking.sub3` | string \| null |  |  |
| `tracking.sub4` | string \| null |  |  |
| `tracking.sub5` | string \| null |  |  |
| `tracking.external_id` | string \| null |  |  |

### SaleChance
| Field | Type | Required | Notes |
|---|---|---|---|
| `order_id` | integer |  |  |
| `percent` | integer |  | min 0; max 100 |
| `level` | `high` \| `mid` \| `low` |  |  |
| `final` | boolean |  | true when the order is already delivered (100) or rejected (0) |
| `factors` | object[] |  |  |
| `factors[].factor` | `final` \| `offer` \| `stage` \| `history` \| `no_history` \| `open_orders` \| `dubl` \| `cpa_rejected` |  |  |
| `computed_at` | string |  | format: date-time |

### Insights
| Field | Type | Required | Notes |
|---|---|---|---|
| `day` | string |  | format: date |
| `yesterday` | object |  |  |
| `week` | object |  |  |
| `pending` | object |  |  |
| `balance` | object |  |  |
| `my_offers` | object[] |  |  |
| `suggested_offers` | object[] |  |  |
| `campaigns` | object[] |  |  |
| `recommendations` | object[] |  |  |
| `recommendations[].type` | `losing_campaign` \| `campaign_problem` \| `stock_out` \| `stock_low` \| `low_sale_offer` \| `try_offers` \| `slowdown` \| `can_withdraw` |  |  |
| `recommendations[].priority` | integer |  |  |
| `recommendations[].hint` | string \| null |  |  |
| `morning_brief` | object \| null |  |  |
| `morning_brief.date` | string |  | format: date |
| `morning_brief.text` | string |  |  |

### Campaign
| Field | Type | Required | Notes |
|---|---|---|---|
| `id` | integer |  |  |
| `name` | string |  |  |
| `status` | `draft` \| `active` \| `paused` \| `archived` |  |  |
| `offer_name` | string \| null |  |  |
| `daily_budget` | number \| null |  | In the ad account currency |
| `delivery_status` | string \| null |  |  |
| `problem` | string \| null |  |  |
| `paused_reason` | string \| null |  |  |
| `stats` | object \| null |  |  |
| `stats.spend_uzs` | integer \| null |  |  |
| `stats.spend_today_uzs` | integer \| null |  |  |
| `stats.orders` | integer \| null |  |  |
| `stats.approved` | integer \| null |  |  |
| `stats.delivered` | integer \| null |  |  |
| `stats.cost_per_order_uzs` | integer \| null |  |  |
| `stats.cost_per_delivered_uzs` | integer \| null |  |  |
| `stats.net_profit_uzs` | integer \| null |  |  |
| `stats.likely_revenue_uzs` | integer \| null |  |  |

### Action
| Field | Type | Required | Notes |
|---|---|---|---|
| `id` | integer |  |  |
| `kind` | `resume_campaign` \| `change_budget` \| `pause_campaign` \| `launch_campaign` \| `create_lead_form` \| `change_creative` |  |  |
| `status` | `pending_confirmation` \| `pending` \| `running` \| `done` \| `failed` \| `cancelled` \| `expired` |  |  |
| `campaign_id` | integer \| null |  |  |
| `title` | string \| null |  |  |
| `details` | object[] |  |  |
| `details[].label` | string |  |  |
| `details[].value` | string |  |  |
| `result` | string \| null |  |  |
| `expires_at` | string |  | format: date-time |
| `created_at` | string |  | format: date-time |
| `decided_at` | string \| null |  | format: date-time |
| `confirmation` | object |  | Only in the 202 response |

### OrderCreate
| Field | Type | Required | Notes |
|---|---|---|---|
| `link_id` | integer | yes | Your link; determines the offer; min 1 |
| `phone` | string | yes | Uzbekistan mobile: +998XXXXXXXXX (other common formats are normalized); max 20 chars |
| `name` | string |  | Customer name (default "Mijoz <last 4 digits>"); max 100 chars |
| `region` | string |  | max 64 chars |
| `variant_id` | integer |  | Defaults to the first in-stock variant; min 1 |
| `variant_type_id` | integer |  | min 1 |
| `external_id` | string |  | Your own id for this order; max 128 chars |
| `click_id` | string |  | max 255 chars |
| `sub1` | string |  | max 255 chars |
| `sub2` | string |  | max 255 chars |
| `sub3` | string |  | max 255 chars |
| `sub4` | string |  | max 255 chars |
| `sub5` | string |  | max 255 chars |

### StatsRow
| Field | Type | Required | Notes |
|---|---|---|---|
| `date` | string |  | format: date |
| `offer_id` | integer |  |  |
| `offer_name` | string \| null |  |  |
| `link_id` | integer \| null |  |  |
| `link_name` | string \| null |  |  |
| `orders` | integer |  |  |
| `approved` | integer |  |  |
| `delivered` | integer |  |  |
| `rejected` | integer |  |  |
| `pending` | integer |  |  |
| `approval_rate_percent` | integer |  |  |
| `delivery_rate_percent` | integer |  |  |
| `earned` | integer |  | Payout of delivered orders, UZS |
| `potential_earnings` | integer |  | Payout of still-open orders, UZS |

### Event
| Field | Type | Required | Notes |
|---|---|---|---|
| `id` | integer |  |  |
| `type` | `order.created` \| `order.status_changed` |  |  |
| `created_at` | string |  | format: date-time |
| `order_id` | integer |  |  |
| `status` | string |  |  |
| `previous_status` | string \| null |  |  |
| `conversion_status` | string |  |  |
| `order` | object |  |  |

### Webhook
| Field | Type | Required | Notes |
|---|---|---|---|
| `id` | integer |  |  |
| `kind` | `json` \| `postback` |  |  |
| `url` | string |  |  |
| `events` | string[] |  |  |
| `description` | string \| null |  |  |
| `is_active` | boolean |  |  |
| `disabled_reason` | string \| null |  |  |
| `consecutive_failures` | integer |  |  |
| `last_success_at` | string \| null |  | format: date-time |
| `last_failure_at` | string \| null |  | format: date-time |
| `created_at` | string |  | format: date-time |

### WebhookCreate
| Field | Type | Required | Notes |
|---|---|---|---|
| `kind` | `json` \| `postback` |  | default: "json" |
| `url` | string | yes | max 2000 chars |
| `events` | `order.created` \| `order.status_changed`[] |  | default: ["order.created","order.status_changed"] |
| `description` | string |  | max 120 chars |

### WebhookPayload
| Field | Type | Required | Notes |
|---|---|---|---|
| `id` | string |  |  |
| `type` | string |  |  |
| `api_version` | string |  |  |
| `created_at` | string |  | format: date-time |
| `data` | object |  |  |

## Order statuses
| status | Meaning |
|---|---|
| `new` | Order received, waiting for the call center. |
| `processing` | Call center is processing the order. |
| `callback` | Customer asked to be called back later. |
| `confirmed` | Customer confirmed the order by phone. |
| `packed` | Order is packed at the warehouse. |
| `shipping` | Order is on the way to the customer. |
| `delivered` | Customer received and paid for the product. Payout is earned. |
| `returned` | Customer refused the product at delivery; it was returned. |
| `cancelled` | Order was cancelled (not confirmed, duplicate, unreachable customer, etc.). No payout. |

### conversion_status (for trackers)
| value | Meaning |
|---|---|
| `lead` | Order received, not yet confirmed (new, processing, callback). |
| `approved` | Confirmed by the customer, not yet delivered (confirmed, packed, shipping). |
| `sale` | Delivered and paid by the customer. Payout earned. |
| `rejected` | Returned or cancelled. No payout. |

### payout_status
| value | Meaning |
|---|---|
| `pending` | Order not delivered yet; payout will be earned on delivery. |
| `earned` | Payout added to the balance. |
| `under_review` | Delivered, payout temporarily held for review. |
| `none` | Order returned/cancelled; no payout. |

## Error codes
| HTTP | error.code | Meaning / what to do |
|---|---|---|
| 400 | `invalid_request` | A parameter or body field is invalid. `error.details` names the field. |
| 400 | `invalid_phone` | phone must be an Uzbekistan mobile number (+998XXXXXXXXX). |
| 400 | `variant_type_required` | The offer has variants — send variant_type_id. |
| 401 | `unauthorized` | No API key or the key is wrong. |
| 401 | `invalid_token` | The OAuth/API token is invalid. |
| 401 | `key_revoked` | The key was revoked in the panel. Create a new one. |
| 401 | `key_expired` | The key expired. Create a new one. |
| 401 | `oauth_token_not_allowed` | OAuth tokens work only on /mcp. Use an API key for REST. |
| 403 | `insufficient_scope` | The key lacks the scope this endpoint needs (see the Scope column). |
| 403 | `ip_not_allowed` | The key is restricted to other IP addresses. |
| 403 | `account_blocked` | The partner account is blocked. |
| 403 | `orders_write_not_allowed` | Submitting orders needs at least 3 delivered orders or admin approval. |
| 404 | `not_found` | Unknown route or object. |
| 404 | `offer_not_found` / `link_not_found` / `order_not_found` / `campaign_not_found` / `action_not_found` / `webhook_not_found` / `variant_not_found` | The object does not exist or does not belong to this account. |
| 409 | `duplicate_order` | This phone already ordered this offer in the last 24 hours. Do not retry. |
| 409 | `customer_order_limit` | This phone already ordered 3 different offers in the last 24 hours. |
| 409 | `link_limit_reached` / `webhook_limit_reached` | Too many links/webhooks on the account. |
| 409 | `ads_unavailable` | Meta ads are not connected for this account. |
| 409 | `pause_failed` / `proposal_rejected` | The Meta action could not be done; see message. |
| 410 | `offer_inactive` | The offer is no longer active. |
| 410 | `out_of_stock` | Out of stock. |
| 415 | `unsupported_media_type` | Send JSON with `Content-Type: application/json`. |
| 429 | `rate_limited` | Too many requests. Wait (see RateLimit-* / Retry-After headers) and retry. |
| 429 | `daily_order_limit` | Daily order limit for the account reached. |
| 429 | `too_many_failed_attempts` | Too many failed auth attempts from this IP. Wait 15 minutes. |
| 500 | `internal_error` | Our error. Retry with backoff; send request_id to support if it repeats. |
| 503 | `offer_temporarily_unavailable` | The offer is waiting for stock. Retry later or pick another offer. |

Rate limits:
- 120 requests/minute per key
- 30 orders/minute per account
- 30 failed attempts per 15 minutes per IP

## Webhooks (signed JSON)
Event types: `order.created`, `order.status_changed`. Create with `POST /webhooks` and `"kind": "json"`. Reply 2xx within 10 s, otherwise we retry (1m, 5m, 15m, 1h, 3h, 6h, 12h, 24h). Events may arrive twice or out of order — deduplicate by `id`.
Header X-Birinchishop-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>. Reject if t is older than 5 minutes.
```js
const crypto = require('crypto');
function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
  const expected = crypto.createHmac('sha256', secret).update(parts.t + '.' + rawBody).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 || ''));
}
```
```python
import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    if abs(time.time() - int(parts["t"])) > 300:
        return False
    expected = hmac.new(secret.encode(), parts["t"].encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))
```

## S2S postbacks (Keitaro, Binom, Voluum…)
Create a webhook with `"kind": "postback"` — we call your URL with GET on each event, replacing macros (URL-encoded): `{click_id}` `{sub1}` `{sub2}` `{sub3}` `{sub4}` `{sub5}` `{order_id}` `{external_id}` `{status}` `{conversion_status}` `{payout}` `{customer_price}` `{currency}` `{offer_id}` `{link_id}` `{event}` `{event_id}` `{timestamp}`
```
https://tracker.example.com/postback?subid={click_id}&status={conversion_status}&payout={payout}&currency={currency}
```
Tracking link template: `https://birinchishop.uz/l/<link_id>?click_id={clickid}&sub1={campaign}` (take `tracking_url_template` from `POST /links`). Payout is earned only on `sale`.

## AI agents (MCP)
Endpoint: `https://birinchishop.uz/api/v1/mcp` (Streamable HTTP, stateless). Claude.ai / ChatGPT: add a custom connector with this URL — OAuth, no key needed. Claude Code:
```bash
claude mcp add --transport http birinchishop https://birinchishop.uz/api/v1/mcp --header "Authorization: Bearer $BSK_KEY"
```
Cursor / generic client config:
```json
{
  "mcpServers": {
    "birinchishop": {
      "type": "http",
      "url": "https://birinchishop.uz/api/v1/mcp",
      "headers": {
        "Authorization": "Bearer ${BSK_KEY}"
      }
    }
  }
}
```
| Tool | Description | Scope |
|---|---|---|
| `get_account` | Get the partner account: name, status, current balance (UZS), frozen balance, minimum withdrawal and the API key scopes. | `account:read` |
| `search_offers` | Search the catalog of active offers (products the partner can promote). Returns customer_price, payout, lead_bonus, stock and sale_rate_percent for each offer. sort=best_selling orders offers by the share of orders that get delivered. | `offers:read` |
| `get_offer` | Get full details of one offer: description, all images, video, variants (sizes/colors with their own price and payout) and the partner's existing links for this offer (my_links). | `offers:read` |
| `list_links` | List the partner's tracking links (streams) with clicks, orders and delivered counts. Each link has a public url to share in ads. | `links:read` |
| `create_link` | Create a new tracking link for an offer. Returns the public url (landing page with order form) and a tracking_url_template that passes click_id/sub1..sub5 for postbacks. Existing links of the partner for an offer are listed in get_offer.my_links. | `links:write` |
| `list_orders` | List the partner's orders with status, payout and payout_status. Filter by status, conversion_status, dates, link, offer or tracking click_id. Phone numbers are masked. | `orders:read` |
| `get_order` | Get one order with its current status, call-center (operator) and courier notes, expected delivery_date and tracking values. | `orders:read` |
| `create_order` | Submit a REAL customer order (lead) for one of the partner's links. A call center will phone this customer. ONLY call this with a real customer's explicit request and their correct Uzbekistan phone number. Never use for testing. The same phone cannot order the same offer twice within 24 hours. Pass idempotency_key to make retries safe. | `orders:write` |
| `get_stats` | Aggregated performance: orders, approved, delivered, rejected, approval/delivery rates, earned and potential earnings (UZS), grouped by day, offer or link. Default range: last 30 days. | `stats:read` |
| `list_events` | Feed of order events (order.created, order.status_changed) in chronological order. Pass the returned meta.next_cursor as `after` to get only new events since the last call. | `events:read` |
| `get_insights` | The partner's personal AI manager view: yesterday's orders/approved/delivered/earnings, last 7 days vs previous week, each promoted offer with sale rate and stock, Meta campaigns (7 days), suggested better-selling offers the partner has not tried, and prioritized recommendations (losing_campaign, stock_out, low_sale_offer, try_offers, slowdown, can_withdraw...). Also returns this morning's brief text. | `insights:read` |
| `get_order_sale_chance` | Estimate (0-100%) that one order will be delivered, with the factors behind it: offer sale rate, current stage, the customer's previous orders across the whole network, duplicates, rejected leads. | `insights:read` |
| `get_ads_status` | Whether the partner's Meta (Facebook/Instagram) ad account is connected, its currency and campaign counts by status. | `ads:read` |
| `list_campaigns` | The partner's Meta ad campaigns with daily budget, delivery status/problems and results for the period: spend, orders, approved, delivered, cost per order, net profit and likely revenue (UZS). | `ads:read` |
| `pause_campaign` | Pause one of the partner's Meta campaigns right away (stops spending). The partner is notified. | `ads:write` |
| `resume_campaign` | Ask the partner to resume a paused campaign. Does NOT resume by itself: sends a confirmation card to the partner's Telegram bot and panel and returns an action (status pending_confirmation); its result is available via get_action. | `ads:write` |
| `change_campaign_budget` | Ask the partner to change a campaign's daily budget. Does NOT change it by itself: sends a confirmation card (Telegram + panel) and returns an action with status pending_confirmation. unit=UZS means the amount is in so'm; ACCOUNT means the ad account currency (often USD). | `ads:write` |
| `get_action` | Status of a confirmation card created by resume_campaign or change_campaign_budget: pending, running, done, failed, cancelled or expired, with the result message. | `ads:read` |

MCP prompts: `daily_review`, `find_offer`, `check_campaigns`, `order_problems`. Ads actions that can increase spending (resume, budget change) never execute directly: they return an action in status pending_confirmation and the partner confirms it in the Telegram bot or panel within 30 minutes. Pausing is immediate.

## Ready prompts
Copy one, attach this file and send it to your AI. / Bittasini nusxa oling, shu faylni biriktiring va AI ga yuboring.

### 1. Telegram bot: new orders and statuses
**EN:** Using the attached Birinchishop API guide, write a Node.js script that polls GET /events every minute (cursor in a local file) and sends me a Telegram message for every new order and every status change (offer name, status in Uzbek, payout). Read BSK_KEY, TELEGRAM_BOT_TOKEN and CHAT_ID from .env.
**UZ:** Ilova qilingan Birinchishop API qo'llanmasi asosida har daqiqada GET /events ni so'raydigan (kursor faylda saqlansin) va har yangi zakaz hamda status o'zgarishini Telegram'ga yuboradigan Node.js skript yozib ber.

### 2. Own landing page → orders
**EN:** Using the attached guide, add an order form to my landing page: the browser posts to MY backend (never expose the API key), my backend calls POST /orders with link_id, phone, name, region, an Idempotency-Key and click_id from the URL. Handle duplicate_order, out_of_stock and rate_limited with friendly Uzbek messages.
**UZ:** O'z landing sahifamdagi forma zakazni MENING serverim orqali POST /orders ga yuborsin (kalit brauzerda ko'rinmasin), Idempotency-Key va click_id bilan; xato kodlariga o'zbekcha xabar chiqsin.

### 3. Daily report to Google Sheets
**EN:** Using the attached guide, write a Google Apps Script that every morning calls GET /stats?group_by=offer for yesterday and appends a row per offer (orders, delivered, earnings, sale rate) to my sheet.
**UZ:** Har kuni ertalab kechagi GET /stats?group_by=offer natijasini Google Sheets'ga qator qilib yozadigan Apps Script yozib ber.

### 4. Keitaro / Binom postback
**EN:** Using the attached guide, tell me exactly which postback URL to add (kind: postback) for my Keitaro tracker so that lead/approved/sale/rejected and payout are passed back, and how to build the tracking link with click_id and sub1..sub5.
**UZ:** Keitaro trekerim uchun qanday postback URL qo'yishimni va click_id/sub1..5 bilan link qanday yasalishini ko'rsat.

### 5. Signed webhook receiver
**EN:** Using the attached guide, write an Express (or FastAPI) endpoint that receives Birinchishop webhooks, verifies X-Birinchishop-Signature with my secret (reject older than 5 minutes), deduplicates by event id and saves orders to SQLite.
**UZ:** Webhook qabul qiladigan, imzoni tekshiradigan, id bo'yicha dublni tashlab, zakazlarni bazaga yozadigan server kodi yozib ber.

### 6. Pick offers for me
**EN:** Using the attached guide, write a script that lists offers with sort=best_selling, in stock, sale_rate_percent >= 25 and payout >= 30000, creates a tracking link for the top 3 (skip if I already have one — check my_links) and prints the links.
**UZ:** Sotuv foizi 25% dan yuqori, daromadi 30 000 dan ko'p 3 ta eng yaxshi offerni topib, linki bo'lmasa yaratib beradigan skript yozib ber.

### 7. AI agent (MCP) setup
**EN:** Using the attached guide, connect the Birinchishop MCP server to Claude Code / Cursor with my key and then give me a daily review: yesterday's results, which offers to push today and which Meta campaigns lose money.
**UZ:** MCP serverni ulab, har kuni ertalab natijalarim va bugun nima qilishim kerakligini aytib beradigan sozlama qilib ber.

---
Support: https://t.me/birinchishop_support — include `request_id` from the response.
