OpenAPI 3.1REST · v1
Dona API
Connect a shop to your own system: products, stock and prices, orders, account health and events — through one REST API. Built for 1C, MoySklad, BILLZ, SmartUp, Linko and AI agents.
The shop owner creates a key in the seller portal: Settings › API. The key is shown once.
- Base URL
https://api.dona.im/seller-api/v1- Key kinds
dona_sk_live_…integration key — your ERP, 1C, POS or own systemdona_ak_live_…AI-agent key — MCP onlydona_it_live_…vendor install token — coming soon
The API is opening in stages. The Status page shows what is on now; the Changelog shows what was added.
/mecurl -sS https://api.dona.im/seller-api/v1/me \
-H "Authorization: Bearer $DONA_KEY"{
"key": { … },
"shop": { … },
"tier": "basic",
"api_access": { … },
"writes_enabled": false,
"limits": {
"requests_per_min": 300,
"burst": 60,
"requests_per_day": 100000,
"writes_per_min": 60,
"writes_per_day": 20000,
"in_flight": 4,
"seller_requests_per_min": 900,
"remaining": { … }
},
"attention": { … },
"docs_url": "https://dona.uz/uz/developers"
}Contents
0108
Quick start
4 stepsFour steps: a key, the first call, a safe rehearsal write, and the event feed.
Create a key
Seller portal › Settings › API › Create key. Tick only the scopes you need. Before creating the first key, the shop owner must accept Annex 2 (API and automated access) in the seller portal.
Until the shop owner accepts the current Annex 2, every door — REST, MCP and OAuth — answers 403 agreement_required with
accept_url, the portal page where it is accepted.Check who you are
GET /mereturns the key, the shop, its tier (Basic/Advanced), scopes and live limits.GET /me curl -sS https://api.dona.im/seller-api/v1/me \ -H "Authorization: Bearer $DONA_KEY"GET /meresponse (the example from the spec)200 { "key": { "id": "0199760c-2f4b-7a3e-9d21-5c8e4b1f0a37", "name": "billz-prod", "kind": "sk", "prefix": "dona_sk_live_3fK", "scopes": [ "catalog:read", "catalog:stock", "orders:read" ], "expires_at": "2027-09-24T00:00:00+05:00", "ip_allow": [] }, "shop": { "id": "0198f3a2-6c1d-7b40-8e55-2a9c3d4e5f60", "name": "Bahor Textile", "status": "approved", "documents_status": "approved", "kyc_status": "approved", "can_sell": true }, "tier": "basic", "api_access": { "state": "enabled", "reason": null }, "writes_enabled": false, "limits": { "requests_per_min": 300, "burst": 60, "requests_per_day": 100000, "writes_per_min": 60, "writes_per_day": 20000, "in_flight": 4, "seller_requests_per_min": 900, "remaining": { "requests_per_min": 287, "requests_per_day": 99412, "writes_per_min": 60, "writes_per_day": 20000 } }, "attention": { "open": 3, "action_required": 2 }, "docs_url": "https://dona.uz/uz/developers" }Rehearse a stock write
With
Dona-Dry-Run: truea write is validated and its effect computed — and nothing is saved.While writes are closed (
writes_enabled), the server answers503 limits_unavailable— not a key error.POST /stock curl -sS -X POST https://api.dona.im/seller-api/v1/stock \ -H "Authorization: Bearer $DONA_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Dona-Dry-Run: true" \ -H "Content-Type: application/json" \ -d '{"items":[{"seller_sku":"A-100","quantity":12}]}'Follow the changes
GET /events?cursor=…is the primary channel. Webhooks push the same events.GET /events curl -sS "https://api.dona.im/seller-api/v1/events?cursor=$LAST_EVENT_ID&limit=200" \ -H "Authorization: Bearer $DONA_KEY"
0208
Reference
14 groups · 61 operationsEvery operation is rendered from the OpenAPI contract when the site is built. The badges show the key tier and the scope each operation needs.
- Basic
- Any valid key
- Advanced
- Identity, documents and shop approved, no active sanction
- Public
- No key
Common parameters (listed by name on each operation)11
Idempotency-Keyheader · string1–255 chars. Scope = this key × route. Terminal 2xx/4xx replayed byte-identical for 24 h; a 5xx is never stored. Missing ⇒
400 invalid_bodywithdetails[{field:"Idempotency-Key",code:"required"}].Accept-Languageheader · stringLocalises
messagein error bodies and single-language renderings. Defaultuz.X-Dona-Integrationheader · stringname/versionof the calling integration; stored (≤ 128 chars) and searchable in the request journal.Dona-Dry-Runheader · stringtrue⇒ validate + guard + compute the effect, then roll back. Zero side-effect rows (no events, no activity, no counters). Same as?dry_run=true. Only the literaltrue/false; any other value ⇒400 invalid_body+details[{field:"Dona-Dry-Run", code:"invalid"}](C51).dry_runquery · stringAlias of the
Dona-Dry-Runheader. Only the literal stringstrue/false—1,0,TRUE,yes, an empty value ⇒400 invalid_body+details[{field:"dry_run", code:"invalid"}](C51). A string enum, not a boolean, so no SDK generator serialises it as1/0.Dona-Sellerheader · string (uuid)Vendor-app install keys only (
dona_it_live_…, S6) — and then REQUIRED on every request, public routes included: the id of the shop the install key belongs to. Missing ⇒400 invalid_body+details[{field:"Dona-Seller", code:"required"}]; sent more than once, or not ONE id in the canonical form the API prints (lower-case, 36 characters — no braces, nourn:uuid:, no padding) ⇒400 invalid_body+details[{field:"Dona-Seller", code:"invalid"}]; naming any other shop — even one that installed the same app ⇒404 not_found(never 403; nothing is read). Ignored on a seller key (dona_sk_).If-None-Matchheader · stringAn
ETagfrom a previous identical request ⇒304with rate headers (cost 0.5).cursorquery · stringOpaque keyset cursor from
next_cursor.limitquery · integerPage size, default 50, max 100 (clamped, with
Dona-API-Warn).limit > 50costs1 + ceil(limit/50).updated_sincequery · stringISO 8601 with offset, or epoch milliseconds. The visible high-water mark is capped at
now() − 10 sso a slow transaction is never skipped.pagequery · integer0-based page alias (Uzum parity) — slower than
cursor, ≤ 1 000 pages; mutually exclusive withcursor.
0308
Limits
Limits are counted per proven key and shop, never per IP. These are the defaults; the numbers that apply to your key are in GET /me → limits.
| Limit | Basic sk | Advanced sk | Agent ak |
|---|---|---|---|
| Requests / min (key) | 300 · burst 60 | 600 · burst 120 | 60 · burst 20 |
| Requests / day (key) | 100 000 | 250 000 | 20 000 |
| Writes / min | 60 | 120 | 10 |
| Writes / day | 20 000 | 50 000 | 2 000 |
| Requests in flight | 4 | 8 | 4 |
| Requests / min (shop, all keys) | 900 | 1 800 | — |
- One product’s stock or price — at most 1 write every 10 seconds (
object_cooldown). - Bulk writes ≤ 1 000 lines; body ≤ 256 KiB (bulk 4 MiB). 100–300 lines recommended.
- Page
limit≤ 100 (≤ 200 on/events).If-None-Match→304at half cost. - Our own 429/503 and 5xx cost nothing and never suspend a key.
Dona-Rate-Limited-Reason values:
key-ratewrite-rateseller-rateconcurrencyobject-cooldownapp-rateglobalsuspended
0408
Error codes
59 codesEvery error is one envelope: error is the stable code, message is localised by Accept-Language.
Branch on the code, never on message.
{
"error": "insufficient_scope",
"message": "Bu amal uchun ruxsat yetarli emas.",
"request_id": "req_01997b2e-9a8b-7c6d-8e5f-4a3b2c1d0e9f",
"details": [],
"doc_url": "https://dona.uz/uz/developers/errors#insufficient_scope",
"required_scope": "orders:write"
}202Accepted
approval_requiredthe MCP door — see MCP
400Bad Request
invalid_bodymalformed JSON, a missing
Idempotency-Key, validation (details[]carries field codes such asikpu_required,cursor_expired), adry_run/Dona-Dry-Runthat is not the literaltrue/false(details[{field, code:"invalid"}], C51), a vendor-app install key withoutDona-Seller(details[{field:"Dona-Seller", code:"required"}], S6) or with a repeated / non-canonical one (code:"invalid"), or an op-specific 400 (invalid_decline_reason)invalid_decline_reasonrequest_state_invalidthe MCP door — see MCP
401Unauthorized
api_key_malformedchecksum fails — refused before any DB read
api_key_expired+
rotate_url, no graceapi_key_revokedalso a vendor-app key whose install was uninstalled — S6
key_kind_not_acceptedan agent key
akon this treedownload_token_invaliddownload_token_expiredinvalid_tokenthe MCP door — see MCP
Also a seller/app/staff JWT on this tree
403Forbidden
insufficient_scope+
required_scope; also a scope indisabled_scopestier_required+
scope; derived on every request; also a key minted for a third-partyorders:piirecipient —details[pii_recipient: pii_third_party_pending_counsel], D7, C59key_suspended+
suspended_until,Retry-After,Dona-Rate-Limited-Reason: suspendedapi_blockedip_not_allowedjudged on the trusted client IP only
seller_not_approvedshop closed/blocked/suspended/restricted
not_a_sellernever on this tree; the portal's owner doors
app_frozenS6: every key of a frozen vendor app, from the next request
agreement_required+
accept_url: the shop owner has not accepted the CURRENT Annex 2 «API and automated access» — the version in force or, while none is, the earliest announced; accepted once in Settings › API, then every door admits the key again. Checked after the sanctions, before the scopesrequest_state_foreignthe MCP door — see MCP
404Not Found
not_foundevery missing OR foreign id (never 403: no existence oracle)
409Conflict
idempotency_in_progress+
Retry-After: 2idempotency_mismatchversion_conflictai_review_openpublish of a product whose Dona AI review case is open — the review decides its status
order_not_acceptableno_pickup_addressorder_not_shippableorder_awaiting_courierorder_not_cancellableorder_has_active_returnno_tracking_number+
ordersno_items_selectedall_items_delayedS4, C53
confirmation_declinednothing written · the MCP door — see MCP
request_state_mismatchthe MCP door — see MCP
request_state_replayedthe MCP door — see MCP
held_write_not_pendingan intent's fate · the MCP door — see MCP
Also a replay of a rejected held write
410Gone
request_state_expiredthe MCP door — see MCP
hold_expiredan intent's fate · the MCP door — see MCP
413Content Too Large
payload_too_largebody > 256 KiB (4 MiB on bulk stock/prices/batch), or > 1 000 lines
422Unprocessable Content
webhook_url_invalid+
details[].code∈timeout,status_<n>,tls,dns,private_address,schemewarehouse_unsupported
429Too Many Requests
rate_limitedwith
Dona-Rate-Limited-Reason: app-rate: a vendor app's bucket, shared by all its installs — S6write_rate_limitedconcurrency_limitedseller_rate_limitedobject_cooldownapi_busyintent_limitthe MCP door — see MCP
Also Always a JSON body + Retry-After from the origin (an edge 429 from Cloudflare has NO body — treat it as Retry-After: 60). Costs 0 and never counts toward the breaker. rate_limited is also the pre-auth ADDRESS guard, answered before the key is read: an address over 1 200 requests a minute that were not admitted (no key, a public route, a malformed, unknown or refused key — admitted requests never count), or one tarpitted at 60/min for 15 min after ≥ 20 unknown keys in a minute. No key is ever suspended by traffic that did not prove it
Also Cloudflare edge, per-IP 60/min on public routes. No JSON body (the edge eats it) — treat as Retry-After: 60
500Internal Server Error
internal_errordetails are in the redacted error copy, never here. Costs 0
503Service Unavailable
attention_busyan acknowledgement that resolves an item could not take the attention writer lock within 2 s — nothing was written; retry after
Retry-After: 2
Also Costs 0
0508
Events and webhooks
41 event typesEvery change lands in one feed. For systems that can only initiate a request (1C), polling it is the primary channel.
Polling
get/eventscurl -sS "https://api.dona.im/seller-api/v1/events?cursor=$LAST_EVENT_ID&limit=200" \
-H "Authorization: Bearer $DONA_KEY"Webhooks — Standard Webhooks signatures
Delivery rules
One event per POST (≤ 20 KB), at-least-once, unordered. Reply 2xx within 10 s (ack first, process after); 3xx is a failure; 410 Gone disables the endpoint. Retry ladder: immediate · 5 s · 5 m · 30 m · 2 h · 5 h · 10 h · 14 h · 20 h · 24 h (≈ 75 h, ±10 % jitter).
Headers
webhook-id- = the event id. Your idempotency key — dedupe for 24 h.
webhook-timestamp- Unix seconds. Reject if |now − ts| > 300 s. A redelivery carries a FRESH timestamp.
webhook-signature- Standard Webhooks: space-delimited
v1,<base64 HMAC-SHA256(base64decode(secret after whsec_), id + "." + ts + "." + rawBody)>; two entries during a 24 h rotation. Dona-Request-Id- Correlates with the delivery log.
Event types
order.createdorder.paidorder.acceptedorder.declinedorder.readyorder.shippedorder.deliveredorder.cancelledorder.line_cancelledorder.accept_due_soonorder.ship_overduereturn.requestedreturn.status_changedproduct.publishedproduct.heldproduct.rejectedproduct.demotedproduct.violationproduct.deletedstock.lowstock.outwrite.heldwrite.approvedwrite.rejectedwrite.expiredaccount.status_changedaccount.documents_decidedaccount.kyc_reopenedaccount.suppressedaccount.health_changedaccount.agreement_owedlive.access_changedlive.strikeattention.openedattention.resolvedkey.expiringkey.suspendedkey.unsuspendedwebhook.failingwebhook.disabledping
The signature below is real: it was computed when the site was built from the demo secret and body shown, and the snippet verifies it byte for byte.
POST https://example.uz/dona-webhook
webhook-id: 01997b2e-7f00-7123-8456-789abcdef012
webhook-timestamp: 1790223151
webhook-signature: v1,CFjQ7u8l+tPV5Wzhawj/Hx9iWYx9sM6mcFJu4wsQxpQ=
# demo secret — NOT a real one
# whsec_ZG9uYS1kZW1vLXNlY3JldC1ub3QtYS1yZWFsLW9uZSE=
{"id":"01997b2e-7f00-7123-8456-789abcdef012","type":"order.created","occurred_at":"2026-09-24T09:12:31+05:00","seller_id":"0198f3a2-6c1d-7b40-8e55-2a9c3d4e5f60","api_version":"v1","data":{"order_id":"01997b2e-41c0-7e6f-8a9b-0c1d2e3f4a5b","status":"paid","payment_status":"paid","payment_method":"card","scheme":"own_fleet","total_uzs":259000,"accept_deadline":"2026-09-25T09:12:00+05:00","ship_by_deadline":null,"updated_at":"2026-09-24T09:12:31+05:00"},"links":{"self":"/seller-api/v1/orders/01997b2e-41c0-7e6f-8a9b-0c1d2e3f4a5b"}}import { createHmac, timingSafeEqual } from 'node:crypto'
// secret = "whsec_…" from POST /webhooks; body = the RAW request body (a string, not parsed JSON)
export function verify(secret, headers, body) {
const id = headers['webhook-id'], ts = headers['webhook-timestamp']
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false
const key = Buffer.from(secret.slice('whsec_'.length), 'base64')
const want = createHmac('sha256', key).update(`${id}.${ts}.${body}`).digest()
return headers['webhook-signature'].split(' ').some((entry) => {
const [v, sig] = entry.split(',')
const got = Buffer.from(sig ?? '', 'base64')
return v === 'v1' && got.length === want.length && timingSafeEqual(got, want)
})
}import base64, hashlib, hmac, time
def verify(secret: str, headers: dict, body: bytes) -> bool:
wid, ts = headers["webhook-id"], headers["webhook-timestamp"]
if abs(time.time() - int(ts)) > 300:
return False
key = base64.b64decode(secret.removeprefix("whsec_"))
want = hmac.new(key, f"{wid}.{ts}.".encode() + body, hashlib.sha256).digest()
for entry in headers["webhook-signature"].split(" "):
v, _, sig = entry.partition(",")
if v == "v1" and hmac.compare_digest(base64.b64decode(sig), want):
return True
return False0608
MCP — AI agents
Claude Code, Cursor, Codex and other AI agents can work with your shop over MCP. Every call is logged.
With an agent key the agent reads the shop: products, orders, events, attention items, account health and these docs. Write tools — stock, prices, shipping and cancelling an order — appear only for an Advanced shop while writes are on. Nothing is written on the first call: the change is confirmed first, in the agent’s own confirmation form or by the shop owner in Settings › API › Held writes.
- Endpoint
https://api.dona.im/seller-api/mcp- Protected resource metadata (RFC 9728)
/.well-known/oauth-protected-resource/seller-api/mcp
The agent key (dona_ak_live_…, scope mcp) is created in Settings › API.
claude mcp add --transport http dona https://api.dona.im/seller-api/mcp --header "Authorization: Bearer ${DONA_AGENT_KEY}"OAuth 2.1 — claude.ai and ChatGPT connectors
Connectors that cannot hold a pasted key sign in with OAuth 2.1: the shop owner approves the connection in the seller portal (Settings › API › Connected apps) and can disconnect it at any time. Over OAuth the agent can only read.
- Authorization server
https://auth.dona.im
0708
SDKs
SDKs for TypeScript, Python, Go, PHP and C# are generated from this same contract.
- TypeScript
- Python
- Go
- PHP
- C#
- Pre-release 0.1.0
The SDKs live in a public GitHub repository. Registry packages (npm, PyPI, NuGet, Packagist) are not published yet — install straight from the repository for now.
TypeScript
Requires Node ≥ 20Needs a local clone
Install git clone https://github.com/ozbuz/dona-api.git && cd dona-api/sdks/typescript && npm installInstall npm install ../dona-api/sdks/typescriptPython
Requires ≥ 3.11Install pip install "dona-api @ git+https://github.com/ozbuz/dona-api.git@main#subdirectory=sdks/python"Go
Requires ≥ 1.26Install go get github.com/ozbuz/dona-api/sdks/go@mainPHP
Requires ≥ 8.1, Guzzle 7Needs a local clone
Install git clone https://github.com/ozbuz/dona-api.git && cd your-project && composer config repositories.dona-api path ../dona-api/sdks/php && composer require ozbuz/dona-api:*@devC#
Requires .NET 10Needs a local clone
Install git clone https://github.com/ozbuz/dona-api.git && dotnet add reference path/to/dona-api/sdks/csharp/src/Dona.Api/Dona.Api.csproj
0808
Changelog and status
Every change is published with its date, kind and the scopes it affects.
Need a key?
The shop owner creates a key in the seller portal: Settings › API. The key is shown once.
A question as a buyer or seller? Visit the help centre.