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.

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

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

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/probe

The 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

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.

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
}

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.

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.