Partner API guide
Catalog with your prices and stock and, if enabled, orders. Full field and response reference: the openapi.yaml specification file (OpenAPI 3.0; opens in Swagger Editor, Postman, Insomnia), attached to this guide. Error message texts are in Russian; rely on code.
1. Quick start
- Open the bot's mini app → Profile → API and create a token: name, expiry, permissions ("catalog only" or "catalog and orders"). The section is visible only if a manager has enabled API access for you.
- The token is shown once. Keep it in a secrets store; never put it in a repository or client-side code. If it leaks, revoke it there and issue a new one.
- Send the token with every request:
Authorization: Bearer fcp_….
export FC_API="https://<host>/api/partner"
curl -H "Authorization: Bearer $FC_TOKEN" "$FC_API/schedule"
Your manager will give you the base address ($FC_API in the examples). The API is server-to-server: browser requests (CORS) are not supported, call it from your backend.
2. General rules
- Price is what you see in the bot (price type, markup, currency from your client card). You never send a price: the server sets it, including quantity tiers.
- Warehouses come from your client card. With a single warehouse you may omit
warehouse. - Working hours. The bot has a schedule: it accepts orders only during working hours. You can get today's hours with
GET /schedule(it always answers, including when the bot is closed); your manager will also tell you the hours.- Order changes (add an item, change quantity, remove a line, cancel the order) are not accepted outside working hours: the response is 423
bot_closed. This always applies. - The catalog (
/catalog,/brands,/categories) is closed outside working hours only if your manager has enabled schedule compliance for you; then it also returns 423bot_closed. If it is not enabled, the catalog is available around the clock. - 423
bot_closedis not a request error. Retry it during working hours; use the sameIdempotency-Keyon the retry: a schedule rejection is not stored, and the action will be executed afresh. - When today's working time is known, it comes right in the response, in
error.details: the opening and closing times and the time zone they are given in. If it is only known that the bot is not accepting requests right now, the response has nodetailsfield. Example:HTTP 423 { "error": { "code": "bot_closed", "message": "Бот закрыт. Сегодня работает с 10:00 до 18:00 (Asia/Dubai)", "details": { "opensAt": "10:00", "closesAt": "18:00", "timezone": "Asia/Dubai" } } }
- Order changes (add an item, change quantity, remove a line, cancel the order) are not accepted outside working hours: the response is 423
- Rate limits are set by your manager. Exceeding them returns 429
rate_limited: wait and retry. Many invalid tokens in a row from one IP also lead to 429. - There is no version in the path. Incompatible changes are shipped as new fields and endpoints: ignore unknown response fields.
3. Error format
HTTP 403
{ "error": { "code": "scope_missing", "message": "…" } }
| HTTP | Codes | What to do |
|---|---|---|
| 400 | bad_request, idempotency_key_required, idempotency_key_invalid | Fix the request |
| 401 | token_missing, token_invalid, token_revoked, token_expired | Check or reissue the token |
| 403 | api_disabled, client_not_allowed, scope_missing, orders_disabled, forbidden, ip_not_allowed | Permission missing: contact your manager |
| 404 | not_found | No such item or order |
| 409 | request_in_progress, order_closed, line_changed, last_line_use_cancel | See "Orders" |
| 422 | order_rejected, onec_error, qty_limit_exceeded, qty_increase_unavailable, serial_line_qty_fixed, idempotency_key_reused | Request understood but not accepted; reason in message |
| 423 | bot_closed | Bot is closed by schedule; retry during working hours |
| 429 | rate_limited | Slow down and retry |
| 503 | order_busy, onec_unavailable | Temporary failure; retry with the same Idempotency-Key |
4. Catalog (catalog:read)
| Request | Returns |
|---|---|
GET /schedule | Whether the bot is open now and today's working hours |
GET /brands | Brands in stock |
GET /categories?brand=… | Categories of a brand |
GET /catalog | Items with prices. Filters: brand, category, warehouse; paging: limit (up to 1000, default 200) and cursor |
GET /catalog/{itemCode} | One item, including the quantity price tiers (tiers) |
The storefront data is refreshed every 30–60 seconds; polling more often is pointless. In all examples below $FC_API and $FC_TOKEN are set as in "Quick start".
GET /catalog parameters
| Parameter | Value | Example |
|---|---|---|
brand | A brand from GET /brands. One value, case-insensitive | Apple |
category | A category from GET /categories?brand=…. One value, case-insensitive | iPhone |
warehouse | Your warehouse code (from the client card); all your warehouses by default | 23 |
limit | Page size: 1 to 1000, default 200 | 100 |
cursor | nextCursor value from the previous response | — |
Filters are combined with AND: brand plus category returns the items of that category of that brand. Pass names with spaces or non-Latin letters via --data-urlencode (with -G curl puts them into the URL itself). If nothing matches, the response is an empty list "items": [], not an error.
Today's working hours: GET /schedule
This request always answers, including when the bot is closed, and does not depend on schedule compliance in your policy. A valid token is all it needs.
curl -sS -H "Authorization: Bearer $FC_TOKEN" "$FC_API/schedule"
{
"open": false,
"timezone": "Asia/Dubai",
"today": { "opensAt": "10:00", "closesAt": "18:00" }
}
| Field | Meaning |
|---|---|
open | true if the bot is accepting requests right now |
timezone | Time zone the working hours are given in |
today.opensAt, today.closesAt | Opening and closing time today, HH:MM |
today | null if there are no working hours today (for example, a day off) or they are not set. Then rely on open. |
Working hours for other days are not returned by this request. If the bot is closed and will not open again today, repeat the request tomorrow.
Brands: GET /brands
curl -sS -H "Authorization: Bearer $FC_TOKEN" "$FC_API/brands"
{ "brands": ["Apple", "Dyson", "Samsung", "Sony"] }
Categories of a brand: GET /categories
curl -sS -G -H "Authorization: Bearer $FC_TOKEN" \
--data-urlencode "brand=Apple" \
"$FC_API/categories"
{ "categories": ["AirPods", "iPad", "iPhone", "MacBook"] }
The brand parameter is required, otherwise 400 bad_request.
Catalog of one brand
curl -sS -G -H "Authorization: Bearer $FC_TOKEN" \
--data-urlencode "brand=Apple" \
--data-urlencode "limit=2" \
"$FC_API/catalog"
{
"items": [
{
"itemCode": "24014",
"name": "iPhone 15 128GB Black",
"brand": "Apple",
"category": "iPhone",
"warehouse": "23",
"currency": "USD",
"price": 345,
"available": 40
},
{
"itemCode": "24159",
"name": "Apple AirPods Max 2 (USB-C) Blue 2026 MHWM4",
"brand": "Apple",
"category": "AirPods",
"warehouse": "23",
"currency": "USD",
"price": 100,
"available": 20
}
],
"nextCursor": "eyJrIjoiMjQxNTl8MjN8IiwibyI6Mn0"
}
A non-null nextCursor means there is a next page (see "Paging through the whole catalog" below). In a list each item has a single price; the quantity price tiers come only in the item card.
Catalog of a brand and category
curl -sS -G -H "Authorization: Bearer $FC_TOKEN" \
--data-urlencode "brand=Apple" \
--data-urlencode "category=iPhone" \
--data-urlencode "limit=100" \
"$FC_API/catalog"
{
"items": [
{
"itemCode": "24014",
"name": "iPhone 15 128GB Black",
"brand": "Apple",
"category": "iPhone",
"warehouse": "23",
"currency": "USD",
"price": 345,
"available": 40
}
],
"nextCursor": null
}
To narrow to one of several warehouses, add --data-urlencode "warehouse=23".
Several categories
The category parameter accepts one value, so for several categories make one request per category and merge the responses on your side. Example: Apple items from iPhone, iPad and AirPods as a single list (requires jq):
for CAT in iPhone iPad AirPods; do
curl -sS -G -H "Authorization: Bearer $FC_TOKEN" \
--data-urlencode "brand=Apple" \
--data-urlencode "category=$CAT" \
--data-urlencode "limit=1000" \
"$FC_API/catalog"
done | jq -s '[ .[].items[] ]'
[
{ "itemCode": "24014", "name": "iPhone 15 128GB Black", "category": "iPhone", "price": 345, "available": 40, "…": "…" },
{ "itemCode": "25101", "name": "iPad 10th gen 64GB Silver", "category": "iPad", "price": 289, "available": 15, "…": "…" },
{ "itemCode": "24159", "name": "Apple AirPods Max 2 (USB-C) Blue 2026 MHWM4", "category": "AirPods", "price": 100, "available": 20, "…": "…" }
]
If a category has more than 1000 items, page through each category by nextCursor (below). To get all categories of a brand without looping, omit category (catalog of the brand).
Paging through the whole catalog
Pass nextCursor from each response as cursor until it is null:
CURSOR=""
while : ; do
RESP=$(curl -sS -G -H "Authorization: Bearer $FC_TOKEN" \
--data-urlencode "limit=500" \
${CURSOR:+--data-urlencode "cursor=$CURSOR"} \
"$FC_API/catalog")
echo "$RESP" | jq -c '.items[]'
CURSOR=$(echo "$RESP" | jq -r '.nextCursor // empty')
[ -z "$CURSOR" ] && break
done
The cursor is an opaque string: do not parse or modify it. If the storefront was refreshed between pages, the export continues from the nearest item without an error; for a strict reconciliation repeat the whole export.
Item card with price tiers: GET /catalog/{itemCode}
curl -sS -H "Authorization: Bearer $FC_TOKEN" "$FC_API/catalog/24014"
{
"items": [
{
"itemCode": "24014",
"name": "iPhone 15 128GB Black",
"brand": "Apple",
"category": "iPhone",
"warehouse": "23",
"currency": "USD",
"price": 345,
"available": 40,
"tiers": [
{ "qty": 1, "price": 345 },
{ "qty": 5, "price": 343 },
{ "qty": 10, "price": 341 }
]
}
]
}
items is an array. Read tiers as "from qty pieces the price per piece is price". Tiers above the available stock are hidden. Discounted items have no tiers. If the item does not exist or is not available to you — 404 not_found.
Stock
Stock is returned in the available field of GET /catalog and GET /catalog/{itemCode}. There is no separate stock request.
Stock permission. Showing stock is enabled by your manager. If it is not granted, available is absent from responses, while the catalog and orders work as usual. To receive stock, contact your manager.
Stock of one item:
curl -sS -H "Authorization: Bearer $FC_TOKEN" "$FC_API/catalog/24014" \
| jq '.items[] | {itemCode, warehouse, available}'
{ "itemCode": "24014", "warehouse": "23", "available": 40 }
Stock for a brand and category (code, warehouse, stock):
curl -sS -G -H "Authorization: Bearer $FC_TOKEN" \
--data-urlencode "brand=Apple" \
--data-urlencode "category=iPhone" \
--data-urlencode "limit=100" \
"$FC_API/catalog" \
| jq '.items[] | {itemCode, warehouse, available}'
{ "itemCode": "24014", "warehouse": "23", "available": 40 }
{ "itemCode": "24020", "warehouse": "23", "available": 12 }
Stock changes with every order. When you add to an order the server checks the quantity again: if stock was not enough, the response has addedQty and capped: true (see "Orders").
Catalog item fields
| Field | Meaning |
|---|---|
itemCode | Item code (used in orders) |
name, brand, category | Name, brand, category |
warehouse | Warehouse code |
currency, price | Currency and your price per piece (with the markup from your client card) |
available | Stock. Absent if stock is not shown to you |
tiers | Price tiers: only in the item card and only if enabled |
markdown, serial, comment | Discounted item: flag, serial number, comment (only if discounted items are available to you; sold one piece at a time) |
warehouseName | Warehouse name (only if allowed) |
5. Orders (orders:write)
Available only if a manager has enabled orders for you and the token was issued with "catalog and orders". An order is the same document as the cart in the bot and the mini app.
| Request | Action | Requires |
|---|---|---|
POST /orders/lines | Add an item to the order | orders enabled |
GET /orders/current | Open order of the current trading session | orders enabled |
GET /orders, GET /orders/{id} | History and a single order with status | orders enabled |
PATCH /orders/{id}/lines/{lineId} | Change a line's quantity (up or down) | allowModify |
DELETE /orders/{id}/lines/{lineId} | Remove a line | allowDelete |
POST /orders/{id}/cancel | Cancel the order | allowCancel |
Add an item
curl -X POST "$FC_API/orders/lines" \
-H "Authorization: Bearer $FC_TOKEN" \
-H "Idempotency-Key: 3f0c2a52-7b8e-4a43-9d1b-2f4a1c9e6b10" \
-H "Content-Type: application/json" \
-d '{"itemCode":"24014","warehouse":"23","qty":5}'
Response 200:
{
"line": {
"lineId": "ln_Qx3v9KpL2mZ1aBcD",
"itemCode": "24014",
"name": "iPhone 15 128GB Black",
"brand": "Apple",
"qty": 5,
"price": 343,
"sum": 1715,
"currency": "USD",
"addedAt": "2026-11-03T10:15:22.000Z"
},
"order": {
"id": "6ae9aefdd12dd7057d4bf68e",
"number": "",
"status": "created",
"currency": "USD",
"warehouse": "23",
"total": 1715,
"cancelled": false,
"readyToShip": false,
"editable": true,
"createdAt": "2026-11-03T10:15:22.000Z",
"updatedAt": "2026-11-03T10:15:22.000Z",
"lines": [ { "lineId": "ln_Qx3v9KpL2mZ1aBcD", "itemCode": "24014", "qty": 5, "price": 343, "sum": 1715, "…": "…" } ]
},
"created": true,
"requestedQty": 5,
"addedQty": 5,
"capped": false
}
Here the price 343 is the tier price for 5 pieces (as in the item card example above). If stock or the per-buyer limit was not enough for everything, you get, for example, "requestedQty": 5, "addedQty": 3, "capped": true: what is available was added to the order. If nothing can be added — 422 order_rejected.
- You do not send a price. The price of the added portion is set by the tier for that quantity.
- If the order already has a line with the same item, serial and price, quantities are summed; if the price differs (for example, the tier changed), a separate line is added.
- Stock may be insufficient: the response then has
cappedand the quantity actually added inaddedQty(requested:requestedQty). A successful response is always 200;createdtells whether a new order was created. - Discounted items require
serial, quantity 1.
Edit and cancel
- Only the current order can be changed or cancelled, and only while it is not closed (ready to ship, invoice issued, blocked or cancelled). Otherwise 409
order_closedwithclosedReasonand the current order in the response. PATCHsets a new quantity for the line ({"qty": 3}), higher or lower than the current one. The line price is recalculated by the tier for the new quantity, solineIdmay change: take it from the response.- On an increase the difference is added with the same checks as
POST /orders/lines(stock, per-buyer limit, quota, item availability). If only part can be added, the quantity grows by what is available and the response has"capped": true; check the line's finalqty. If nothing can be added — 422order_rejectedand the order is unchanged. - Lines with a serial number (discounted items) cannot be changed — 422
serial_line_qty_fixed. To remove a line useDELETE. - Take
lineIdfrom the current order. It depends on the line's content and changes together with the price; if the line changed or was removed — 409line_changedwith the current order. - The last line cannot be removed (409
last_line_use_cancel) — cancel the order instead.
Current order: GET /orders/current
curl -sS -H "Authorization: Bearer $FC_TOKEN" "$FC_API/orders/current"
{
"order": {
"id": "6ae9aefdd12dd7057d4bf68e",
"number": "",
"status": "created",
"currency": "USD",
"warehouse": "23",
"total": 502,
"cancelled": false,
"readyToShip": false,
"editable": true,
"createdAt": "2026-11-03T09:01:10.000Z",
"updatedAt": "2026-11-03T09:14:55.000Z",
"lines": [
{ "lineId": "ln_g_I2gvqZXVc9Y7DK", "itemCode": "24159", "name": "Apple AirPods Max 2 (USB-C) Blue 2026 MHWM4",
"brand": "Apple", "qty": 3, "price": 100, "sum": 300, "currency": "USD" },
{ "lineId": "ln_eDECuLIGBiTGCs9x", "itemCode": "23972", "name": "Apple AirPods Max 2 (USB-C) Midnight 2026 MHWK4",
"brand": "Apple", "qty": 2, "price": 101, "sum": 202, "currency": "USD" }
]
}
}
If there is no open order in the current trading session, the response is { "order": null }.
List and single order: GET /orders, GET /orders/{id}
curl -sS -G -H "Authorization: Bearer $FC_TOKEN" --data-urlencode "limit=20" "$FC_API/orders"
curl -sS -H "Authorization: Bearer $FC_TOKEN" "$FC_API/orders/6ae9aefdd12dd7057d4bf68e"
The list returns { "items": [ …orders… ], "nextCursor": "…" | null } (paged like the catalog, limit up to 100); a single order is an object in the same shape as order above. Someone else's or a non-existent order — 404 not_found.
Order fields
| Field | Meaning |
|---|---|
id, number | Order identifier; document number in the accounting system (empty string until assigned) |
status | Order status (for example created; a cancelled order has a cancelled status) |
currency, warehouse, total | Currency, warehouse code, total of the lines |
cancelled, readyToShip | Cancelled; ready to ship |
editable, closedReason | Whether the order can be changed through the API; if not — the reason (cancelled, shipped, ready_to_ship, collecting, closed, unsupported) |
lines[] | Lines: lineId, itemCode, name, brand, qty, price, sum, currency, serial (discounted items), addedAt |
Line identifier: lineId
lineId is the address of an order line in PATCH and DELETE requests. It is created by the server: you do not invent it, you take it from a response (lines[].lineId in POST /orders/lines, GET /orders/current, GET /orders, GET /orders/{id} and in responses to changes).
- It is an opaque string. Do not parse it or build it yourself; do not use it as a sequence number: line order and numbering are not guaranteed.
- It depends on the line's content (item, serial, price) and changes together with the price. After a quantity change the price may be recalculated by the tier scale and the line gets a new
lineId: after everyPATCHtake it from the response. - Identical lines (same item, serial and price) are not merged; repeats get a suffix like
~2added tolineId. It is part of the value, pass it whole. - If a
lineIdis stale (the line was already changed or removed, including by a manager), the server responds 409line_changedwith the current order in the body. Take the newlineIdfrom it and repeat the action with a newIdempotency-Key. - A line is looked up only inside your client's current order; other orders and lines are not accessible.
Change a line's quantity: PATCH
Increase line 23972 from 2 to 5 pieces (ORDER is the order id, LINE is the lineId from GET /orders/current):
curl -X PATCH "$FC_API/orders/$ORDER/lines/$LINE" \
-H "Authorization: Bearer $FC_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"qty":5}'
{
"order": {
"id": "6ae9aefdd12dd7057d4bf68e",
"total": 800,
"editable": true,
"lines": [
{ "lineId": "ln_g_I2gvqZXVc9Y7DK", "itemCode": "24159", "qty": 3, "price": 100, "sum": 300, "…": "…" },
{ "lineId": "ln_xLW2qtzgku-2nn6t", "itemCode": "23972", "qty": 5, "price": 100, "sum": 500, "…": "…" }
],
"…": "…"
},
"changed": true
}
In the example the line price changed from 101 to 100: a different tier applies for 5 pieces, and the whole line is repriced by it. That is why lineId changed too — take it from the response for the next requests. The line stays a single line: the added pieces are merged into it, no second line is created.
If stock or the per-buyer limit was not enough for everything (you asked for 500, only 10 are possible), the line grows to the available quantity and the response has "capped": true:
{ "order": { "…": "…", "lines": [ { "itemCode": "23972", "qty": 10, "price": 99, "…": "…" } ] }, "changed": true, "capped": true }
Decreasing works the same way ({"qty":3}) and the stock reservation is released. A request with the same quantity returns "changed": false.
Remove a line: DELETE
curl -X DELETE "$FC_API/orders/$ORDER/lines/$LINE" \
-H "Authorization: Bearer $FC_TOKEN" \
-H "Idempotency-Key: $(uuidgen)"
{ "order": { "id": "6ae9aefdd12dd7057d4bf68e", "total": 500, "lines": [ { "itemCode": "23972", "qty": 5, "…": "…" } ], "…": "…" }, "changed": true }
Cancel an order: POST /orders/{id}/cancel
curl -X POST "$FC_API/orders/$ORDER/cancel" \
-H "Authorization: Bearer $FC_TOKEN" \
-H "Idempotency-Key: $(uuidgen)"
{ "order": { "id": "6ae9aefdd12dd7057d4bf68e", "status": "Отменён", "cancelled": true, "editable": false, "closedReason": "cancelled", "…": "…" }, "changed": true }
Order error examples
Stale lineId (the line was already changed or removed): 409 with the current order in the response — take the new lineId from it and repeat with a new key.
HTTP 409
{
"error": { "code": "line_changed", "message": "Строка заказа изменилась, перечитайте заказ" },
"order": { "id": "6ae9aefdd12dd7057d4bf68e", "lines": [ "…" ], "…": "…" }
}
No stock or limit exhausted (the requested quantity is not available at all): 422, the order is unchanged.
HTTP 422
{ "error": { "code": "order_rejected", "message": "Товар недоступен в нужном количестве" } }
Action not allowed for you (for example removing lines is switched off by your manager): 403.
HTTP 403
{ "error": { "code": "forbidden", "message": "Удаление строк заказа через API отключено для этого клиента" } }
Last line: 409 last_line_use_cancel. No Idempotency-Key header: 400 idempotency_key_required. qty below 1: 400 bad_request (to remove a line use DELETE).
Numbers, item codes, prices and identifiers in the examples are illustrative: your values depend on your catalog and client settings. The message texts are in Russian; rely on code.
6. Idempotency-Key
Why it exists. If a request is interrupted (network, timeout) and you did not get a response, you cannot tell whether the action was applied. Retrying blindly is dangerous: a line may be added twice. Idempotency-Key solves this: a retry with the same key is not executed a second time, and the response of the first request is returned instead.
Which requests. All order-modifying requests require the Idempotency-Key header: POST /orders/lines (add an item), PATCH …/lines/{lineId} (change quantity), DELETE …/lines/{lineId} (remove a line) and POST …/cancel (cancel the order). Without it: 400 idempotency_key_required. GET requests (catalog, reading orders) do not need a key.
Who creates the key. The key is created by your system before the request is sent; the server does not issue keys. Use a new key for each new action (for example a UUID v4: uuidgen on Linux, or your language's UUID function), or a key derived from your own document, such as order-1042-line-3, so that re-sending the same action from your system automatically gets the same key. Store the key together with the action until you get a response: you need it for a retry.
KEY=$(uuidgen)
curl -sS -X PATCH "$FC_API/orders/$ORDER/lines/$LINE" \
-H "Authorization: Bearer $FC_TOKEN" \
-H "Idempotency-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"qty": 5}'
# no response received — retry: same $KEY, same body
curl -sS -i -X PATCH "$FC_API/orders/$ORDER/lines/$LINE" \
-H "Authorization: Bearer $FC_TOKEN" \
-H "Idempotency-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"qty": 5}'
# → 200 and header Idempotent-Replay: true, the quantity was not added a second time
- One key — one action. 8–128 characters allowed:
A-Za-z0-9_.:-(otherwise 400idempotency_key_invalid). A new action needs a new key; do not use one key for different requests. - If you did not get a response (disconnect, timeout), repeat the same request with the same key: the action will not run twice and the stored response is returned with
Idempotent-Replay: true. Do not generate a new key for each retry. - A key is unique per token and stored for 24 hours.
- Same key with a different request — 422
idempotency_key_reused. - Same key while the first request is still running — 409
request_in_progress; wait (seeRetry-After) and retry. If this lasts over a minute (the first request was interrupted on our side), readGET /orders/currentand, if needed, repeat the action with a new key. - Business rejections (409, 422) are stored and replayed. Temporary ones (423, 429, 503) and internal errors are not: retry them with the same key.
7. FAQ
- 403
api_disabled/orders_disabled. API or orders are not enabled for you by a manager. - 422
order_rejected(item unavailable in the requested quantity). No stock, quota exhausted or the item is not available to you — seemessage. - 422
onec_error. The accounting system did not accept the change; the order is unchanged. Contact your manager if it repeats. - The token stopped working. Check expiry and revocation in Profile → API; the 401 code tells what exactly happened.