Platitun · Seller API
Seller API
Endpoint reference by section
Every endpoint has its own page: request and response fields, refusal reasons with what to do about them, and an example.
- Catalogue and requests
catalog· 7 - Listings
listings· 12 - Auto-delivery stock
stock· 2 - The deal
orders· 16 - Figures and rating
stats· 3 - Payouts
payouts· 1 - Documents and payout details
documents· 6 - Access keys
keys· 3 - Event subscription
hooks· 9
What this is for
An access key lets a program run your stock: create cards, switch them on, refill the auto-delivery stock and read sales figures. Everything here can also be done with buttons in the cabinet — the API is for sellers with hundreds of cards.
The first key is issued in the seller cabinet, section “Program keys”; after that a program can issue keys itself, with a key carrying the `keys` scope. A key is shown once: we do not keep it — only a hash is stored, and we cannot recover it. Lost it — revoke and issue a new one.
The service catalogue is read-only. Only the marketplace edits it, and that is deliberate: a buyer cannot tell two differently named “Cursor Pro” entries apart. If a position is missing, file a request in the cabinet — a human reviews it.
Presenting the key
- Header `Authorization: Bearer ptk_…` — the usual way.
- Or `X-Api-Key: ptk_…` if your tool cannot set the first one.
- Both are equal. The key is NOT accepted in the query string: URLs end up in proxy logs.
A key carries scopes: catalog, listings, stock, stats, orders, payouts, documents, keys, hooks. Tick only what you need — a leaked key can do exactly what is ticked. The “who am I” endpoint works with any key that has at least one scope.
Rules common to all endpoints
- Rate: 60 requests per minute per key, short bursts up to 120. Above that — 429 with a `Retry-After` header.
- Every changing request (POST, PATCH, DELETE) must carry an `Idempotency-Key`. A repeat with the same key and the same body returns the earlier response instead of creating a second record. The same key with a different body is refused rather than silently substituted.
- Lists are paged by cursor: the response carries `nextCursor`, pass it in the next request. Default page 50, maximum 200. There are deliberately no page numbers: the list changes while you page through it.
- Money is whole kopecks (`priceMinor`). There are no fractional roubles anywhere.
- Times are UTC strings. No local time appears in responses.
- From outside, a card is identified by its public NUMBER — the same one in its storefront address. Internal ids are never exposed.
A refusal arrives as a code in the `error` field: no_key, bad_key, revoked, not_seller, seller_blocked, scope, rate, idempotency_key_required, idempotency_mismatch, not_found, bad_request. A card-rule refusal arrives as `{"error":"rejected","refusal":{…}}` naming the exact reason.
Endpoints
Every endpoint there is
GET /api/v1/me
GET /api/v1/catalog/services?q=&category=&limit=&cursor=
GET /api/v1/catalog/services/{slug}
GET /api/v1/catalog/usual-price?service=&productType=
GET /api/v1/catalog/demand?service=&productType=&top=
GET /api/v1/catalog/requests?limit=
POST /api/v1/catalog/requests
GET /api/v1/listings?active=&service=&limit=&cursor=
POST /api/v1/listings
GET /api/v1/listings/{number}
PATCH /api/v1/listings/{number}
DELETE /api/v1/listings/{number}
POST /api/v1/listings/{number}/state
POST /api/v1/listings/{number}/copy
POST /api/v1/listings/{number}/test
PUT /api/v1/listings/{number}/image
DELETE /api/v1/listings/{number}/image
GET /api/v1/listings/{number}/stock
POST /api/v1/listings/{number}/stock
GET /api/v1/orders?status=&needsAction=&updatedSince=&limit=&cursor=
GET /api/v1/orders/{id}
POST /api/v1/orders/{id}/confirm
POST /api/v1/orders/{id}/decline
POST /api/v1/orders/{id}/deliver
POST /api/v1/orders/{id}/messages
GET /api/v1/orders/{id}/handover
POST /api/v1/orders/{id}/handover
POST /api/v1/orders/{id}/handover/reveal
POST /api/v1/orders/{id}/dispute/reply
GET /api/v1/payouts?limit=&cursor=
GET /api/v1/stats?period=today|week|month|all&sort=revenue
GET /api/v1/clients?limit=&skip=
GET /api/v1/rating
GET /api/v1/questions?onlyUnanswered=&limit=
POST /api/v1/questions/{id}/answer
GET /api/v1/reviews?limit=
POST /api/v1/reviews/{id}/reply
GET /api/v1/complaints?limit=
POST /api/v1/complaints
GET /api/v1/work-mode
PUT /api/v1/work-mode
GET /api/v1/papers
PUT /api/v1/papers/nickname
POST /api/v1/papers/offer
PUT /api/v1/papers/status
POST /api/v1/papers/registry-confirm
PUT /api/v1/papers/payout
GET /api/v1/keys
POST /api/v1/keys
DELETE /api/v1/keys/{id}
GET /api/v1/hooks
PUT /api/v1/hooks
DELETE /api/v1/hooks
POST /api/v1/hooks/secret
POST /api/v1/hooks/resume
POST /api/v1/hooks/test
GET /api/v1/hooks/deliveries?limit=&cursor=
GET /api/v1/hooks/probe?limit=
POST /api/v1/hooks/probeThe catalogue tells you what fits what: a service response carries `deliveryMethods` (which delivery methods each product type allows), `slaChoices` (allowed delivery times in minutes) and `guaranteeChoices` (allowed guarantee periods in days). No guessing — the marketplace validates against the same values.
A card update sends the WHOLE body: card rules are interlocked — delivery method depends on product type, amount and rate on the credits type — and a single field cannot be validated in isolation. The service and plan cannot be changed by an update: that would be a different card.
Deletion works only while the card has no orders: an order must stay self-contained for a dispute a month later. A card with history is switched off instead, and the response says so plainly — `result` is `hidden`, not `deleted`.
Stock secrets are never returned — not to you, not with your key. They are stored encrypted and revealed only to the buyer at delivery. The stock endpoint shows how many units are free and how many were issued.
Examples
Create a card
POST /api/v1/listings
Authorization: Bearer ptk_…
Idempotency-Key: my-upload-2026-08-21-0001
Content-Type: application/json
{
"service": "cursor",
"tariff": "pro",
"productType": "subscription",
"deliveryMethod": "no_login_topup",
"priceRub": 2500,
"slaMinutes": 30,
"guaranteeDays": 14,
"stock": 3,
"description": "Оплата на ваш аккаунт, пароль не передаётся.",
"confirmRequired": true
}
→ 201 { "number": 1042, "isActive": false, "isTest": false }A card is born SWITCHED OFF — through the API as well. Switching on stays a separate action because that is what turns your price into a public offer you are bound to honour.
Switch a card on
POST /api/v1/listings/1042/state
Idempotency-Key: turn-on-1042
{ "active": true }
→ 200 { "number": 1042, "isActive": true, "autoIssue": false }Refill the auto-delivery stock
POST /api/v1/listings/1042/stock
Idempotency-Key: stock-1042-batch-7
{ "items": ["CODE-AAA-111", "CODE-BBB-222"] }
→ 201 { "number": 1042, "added": 2, "free": 5 }Auto-delivery can only be switched on when the stock is not empty: promising instant delivery without stock is a lie to the buyer, and it costs a lot in the auto-delivery rating.
The deal
- The `orders` scope covers everything a seller does on an order: take it, decline it, mark delivery, message the buyer, put credentials into the vault, read what the buyer put there, answer a dispute.
- `needsAction=true` returns the orders where the move is yours: awaiting your confirmation, awaiting delivery, dispute open.
- `updatedSince` is the fallback for webhooks: it is always there, and polling it is more reliable than waiting for delivery.
- For a subscription or an account with a subscription, delivery REQUIRES `nextChargeDay` — the next charge date. It is the proof of activation, and the buyer verifies it in seconds.
- A delivery response may carry `noteRejected`: the delivery happened, the note did not pass the contact filter. The note was not posted — rewrite it without contacts and send it as a message.
- Resolving a dispute is not in the API: that is a human reviewing someone else’s money. Your answer to a dispute IS here, and it matters — twelve hours of silence closes the dispute with a refund on its own.
Mark a subscription as delivered
POST /api/v1/orders/cmt2x34ms001boz019p80jgzn/deliver
Idempotency-Key: deliver-1042-once
{
"nextChargeDay": "2026-09-20",
"note": "Подписка активна, продление автоматическое."
}
→ 200 { "order": "cmt2…", "status": "delivered", "noteRejected": null }🔴 The vault: the list of an order’s vaults contains NO secrets — only “mine or theirs” and “was it opened”. A vault left by the other side can be read exactly ONCE, with a separate request; a repeat honestly answers “already opened”. Your own vault cannot be read at all: the marketplace does not read vault contents.
🔴 Who bought is NOT named in responses — no email, no nickname. The marketplace does not show this to the seller in the cabinet either: the “Buyers” section works without personal data. The API is not a way around what the interface closes.
🔴 The one thing the API cannot and will not do: claim that you are PRESENT. Presence is determined by the marketplace itself, and the answer to “Call the seller” is the fact of your entering the cabinet. Otherwise “present” would stop meaning anything: a buyer would be choosing a promise made by a program rather than a person at a computer. Work mode — capacity, do-not-disturb, quiet hours — can be set through the API.
Documents, payout details and keys
🔴 Read this before ticking the `documents` and `keys` scopes. A key with `documents` can rewrite the account you get paid to and accept the seller offer on your behalf. A key with `keys` can issue new keys. A leaked key with either scope is access to your money, not to your listings. A working key that uploads stock does not need them — do not tick them without a reason.
- The checks are THE SAME as in the cabinet: the INN checksum, the account checksum against the BIC, and the name verification against the tax registry. None is relaxed for the convenience of a program.
- The company name is NOT accepted from the request at all: the marketplace fetches it from the registry by INN and you confirm it is you. Otherwise the protection against a mistyped digit and against someone else’s number would not work.
- A failed verification returns 409 with the reason inside `refusal`. For an ALREADY WORKING seller the previous data is left untouched: `keptPrevious` is `true`, trading continues, and a request goes to an administrator.
- After a successful verification look at `needsConfirm`: until the name is confirmed (`POST papers/registry-confirm`) your admission is not complete and your stock will not reach the storefront.
- A seller blocked or suspended by an administrator cannot lift it themselves — neither by button nor by program. The registry confirms that a company exists, not the right to trade here.
- Phone-based instant payments are only for the self-employed: an LLC or a sole trader is paid to a bank account. An LLC must also provide a KPP.
- The account number is returned only as its last four digits. You can rewrite it; you cannot read it in full: a leaked response must not become leaked banking details.
- A key cannot grant scopes it does not hold itself. Otherwise one leaked key with the `keys` scope would become full access, and revoking would not help. A key with any scopes is issued from the cabinet.
- A key may revoke ITSELF — that is how a program that spotted a leak closes the door immediately. The next request with that key is refused.
- An issued key is shown ONCE, in the response to the issuing request. There is no second showing: only a hash is stored, and we do not know the key ourselves.
Admission: INN, confirmation, payout details
PUT /api/v1/papers/status
Idempotency-Key: status-2026-08-21
{ "sellerType": "ip", "inn": "323402607218" }
→ 200 { "registryName": "ИП Иванов Иван Иванович", "needsConfirm": true }
POST /api/v1/papers/registry-confirm
Idempotency-Key: confirm-2026-08-21
→ 200 { "confirmed": true, "already": false }
PUT /api/v1/papers/payout
Idempotency-Key: payout-2026-08-21
{
"method": "account",
"account": "40802810400000000160",
"bik": "044525225",
"name": "ИП Иванов Иван Иванович"
}
→ 200 { "method": "account", "accountTail": "0160", "filled": true }Issue a key and revoke it
POST /api/v1/keys
Idempotency-Key: key-for-uploader-1
{ "name": "заливка склада", "scopes": ["listings", "stock"] }
→ 201 { "key": "ptk_…", "prefix": "ptk_abcd1234", "scopes": ["listings","stock"] }
DELETE /api/v1/keys/cmt2x34ms001boz019p80jgzn
Idempotency-Key: revoke-uploader-1
→ 200 { "revoked": true, "id": "cmt2x34ms001boz019p80jgzn" }Event subscription (webhooks)
The marketplace calls your address when something happens: a new order, a payment, a dispute, a buyer message, an empty stock. Polling stays forever: `updatedSince` on orders always works and is more reliable than waiting for a delivery. A webhook is a convenience, not a promise: someone else’s address can stay down for a day.
Set up a subscription
PUT /api/v1/hooks
Idempotency-Key: hooks-2026-08-21
{
"url": "https://hooks.example.com/platitun",
"events": ["order.new", "order.paid", "order.message", "stock.out"]
}
→ 200 {
"url": "https://hooks.example.com/platitun",
"events": ["order.new","order.paid","order.message","stock.out"],
"secret": "whs_…" // показан ОДИН раз, только при создании подписки
}What an event looks like
POST https://hooks.example.com/platitun
x-platitun-event: order.paid
x-platitun-delivery: cmt2x34ms001boz019p80jgzn
x-platitun-timestamp: 1787000000
x-platitun-attempt: 1
x-platitun-signature: sha256=9f2a…
{
"event": "order.paid",
"at": "2026-08-21T14:03:11.512Z",
"data": { "order": "cmt2x34ms001boz019p80jgzn", "slaMinutes": 30 },
"delivery": "cmt2x34ms001boz019p80jgzn",
"attempt": 1
}- Events: order.new, order.paid, order.auto_issued, order.cancelled, order.confirmed, order.completed, order.message, dispute.opened, dispute.resolved, stock.low, stock.out, call.new, question.new, review.new. Tick only what you need — every event is a request to your service.
- The signing secret is issued by the marketplace and shown ONCE, when the subscription is created. Editing the address does not change the secret: your signature check is already configured, and changing the secret for you would break it silently. Rotating the secret is a separate request, and the previous one dies immediately.
- Signature: `sha256=HMAC-SHA256(secret, "timestamp.body")` in the `x-platitun-signature` header. The timestamp is in `x-platitun-timestamp`, seconds. Verify BOTH the signature and the freshness: a signature without a time check protects against forgery but not against replay.
- 6 attempts, all within 51 minutes: at once, then after 10 seconds, a minute, 5, 15 and 30 minutes. Any 2xx answer counts as success; we do not follow redirects.
- After the sixth failure delivery is PAUSED and you get a message. You resume it yourself — `POST /api/v1/hooks/resume` or a button in the cabinet: an address that stayed down for an hour is usually still down a day later.
- Events that happen during a pause sit in the log with status `skipped_paused`. They are NOT re-sent when you resume: an avalanche of stale events is worse than their absence — a program would take cancelled orders for new ones. Pick them up by polling.
- Answer 200 IMMEDIATELY and do the work afterwards. We wait ten seconds; a slow answer counts as a failure and we will retry — you would receive the same event twice.
- The same event can arrive twice (a retry after a broken connection). The key is `delivery` in the body and the header carrying it: if you have handled that id, skip the repeat.
- Who bought is NOT named in events — no email, no nickname. The marketplace does not show this to the seller in the cabinet either.
- The address must be `https` and public. Internal and service addresses are refused, and this is checked before EVERY send: a name that looked harmless when saved can point anywhere by the time we send.
Verifying the signature on your side
// Проверка подписи у себя (Node.js). Тот же расчёт, что у площадки.
import { createHmac, timingSafeEqual } from 'node:crypto';
app.post('/platitun', express.raw({ type: '*/*' }), (req, res) => {
const ts = Number(req.get('x-platitun-timestamp'));
const got = req.get('x-platitun-signature') ?? '';
const want = 'sha256=' + createHmac('sha256', process.env.PLATITUN_HOOK_SECRET)
.update(ts + '.' + req.body.toString('utf8')).digest('hex');
// Сравнение постоянным по времени способом: обычное сравнение строк
// заканчивается на первом различии, и по времени ответа подпись подбирается.
const ok = got.length === want.length && timingSafeEqual(Buffer.from(got), Buffer.from(want));
// Свежесть — обязательна: подпись без проверки времени защищает от подделки,
// но не от повтора вчерашнего запроса.
const fresh = Math.abs(Date.now() - ts * 1000) < 300000;
if (!ok || !fresh) return res.sendStatus(401);
res.sendStatus(200); // отвечайте СРАЗУ, работайте потом
});🔴 You can check your side without waiting for a live order: `POST /api/v1/hooks/test` sends a test event the same way — same signature, same log, same retry ladder. Such an event carries `test: true` in the body so a program does not take the probe for a real order. The delivery log is `GET /api/v1/hooks/deliveries`: the event, the attempt count, the answer code and the error text.
MCP server: the same actions as tools
If your stock is run by an AI agent rather than your own program, it does not need to be taught endpoints — the marketplace has an MCP server. One address: `https://platitun.ru/api/mcp`, the same key (`Authorization: Bearer ptk_…`), the conversation is JSON-RPC 2.0 over a single POST to that address. There is no separate entrance for agents: two entrances would mean two places where revoking a key might not work.
- A tool contains NO action: it calls the same API endpoint your own program would. The same guard, the same scope, the same repeat check, the same log — because it is literally the same endpoint.
- The set is curated: catalogue, listings, stock, the deal, chat, figures, questions, reviews, payouts. Given fifty tools at once an agent chooses worse, so everything else is reachable through the universal `platitun_api_request` tool, with the endpoint list available as the `platitun://api/endpoints` resource.
- Every tool states plainly whether it READS or CHANGES, and which key scope it needs. An agent may call the reading ones on its own; the changing ones should be confirmed by a human.
- Changing tools accept an `idempotencyKey` argument. If omitted, the key is derived from the tool name and the arguments: an accidental repeat of the same call will not create a second order. Set your own value if you deliberately repeat an identical action.
- A marketplace refusal arrives as the tool RESULT marked as an error, not as a broken conversation: the agent should read the reason and fix the call rather than decide the server is down.
- The server does not open an event stream: answers are short, and an open stream is state you have to repair after every disconnect. Events are pushed by webhooks (see the section above).
- There is no tool to claim the seller is present — no tool and no endpoint. The marketplace determines presence itself.
Talking to the MCP server
POST https://platitun.ru/api/mcp
Authorization: Bearer ptk_…
Content-Type: application/json
{ "jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": { "protocolVersion": "2025-06-18",
"clientInfo": { "name": "my-agent", "version": "1.0" } } }
→ { "jsonrpc":"2.0","id":1,"result":{
"protocolVersion":"2025-06-18",
"capabilities":{"tools":{},"resources":{}},
"serverInfo":{"name":"platitun","version":"1.0.0"},
"instructions":"Инструменты работают от имени продавца, чей ключ предъявлен…" } }
{ "jsonrpc": "2.0", "id": 2, "method": "tools/list" }
{ "jsonrpc": "2.0", "id": 3, "method": "tools/call",
"params": { "name": "platitun_orders", "arguments": { "needsAction": true } } }
→ { "jsonrpc":"2.0","id":3,"result":{
"content":[{"type":"text","text":"{ \"items\": [ … ] }"}],
"isError": false } }What is not here yet
The seller side is ALL here, with one exception: presence cannot be claimed (see the deal section). What remains is a page per endpoint with request and response fields and the full list of refusal reasons; today every endpoint has a line in the list and a section of rules, but not a page of its own. The order is deliberate: each piece ships and is verified on its own, so a failure rolls back a part rather than everything.
Keys are issued in the seller cabinet: Sales → Program keys.