API reference
Platform API conventions: base URL, authentication, Idempotency-Key, pagination, limits, headers, versioning, errors, and the PII policy.
- Base URL
https://dukkan.one/platform-api/v1- Version
1.0.0- OpenAPI
- OpenAPI specification
On this page
This page collects the conventions that apply to every Platform API operation; the resource pages that follow are generated from the same OpenAPI specification the server runs. Read it once, then come back for any number.
Base URL#
https://dukkan.one/platform-api/v1The API is served on the main host only; storefront subdomains and custom domains return 404. Every response is application/json with Cache-Control: no-store.
Authentication#
Authorization: Bearer YOUR_ACCESS_TOKENAn app access token from Authorization, bound to one store and one install. Every operation requires a specific effective scope listed in the scope reference; a missing one returns 403. A missing, expired or revoked token returns 401, as does the granting member losing access to the store.
Idempotency-Key#
Every operation that creates or changes store data (PATCH /orders/{id}/status, POST /orders/{id}/fulfillments, PATCH /orders/{id}/fulfillments/{fulfillmentId}, POST /orders/{id}/refunds, POST /inventory/movements) requires the header:
Idempotency-Key: booking-6f1d2c3b-1- A string of 1 to 200 characters, unique per install. Omitting it returns
400. - Repeating the request with the same key and the same payload returns the original response and status with the header
Idempotency-Replayed: true. - The same key with a different payload returns
409with the messageIdempotency key was already used for a different request; a request still in progress also returns409. - Recorded responses are kept for 24 hours.
PUT /webhooks and DELETE /webhooks are idempotent by nature and do not take the key.
Pagination#
Lists return { "data": [...], "next_cursor": "..." }:
| Parameter | Rule |
|---|---|
limit |
1–100, default 50 |
cursor |
opaque, up to 512 characters, from the previous next_cursor |
next_cursor |
null at the end of the list |
The cursor keys on the immutable created_at, so it stays stable while data changes. For incremental sync use the created_at_min and updated_at_min filters (orders) and updated_at_min (products), and keep the last timestamp you processed.
Rate limits#
| Limit | Value |
|---|---|
| Requests per install | 180 per minute |
| Writes per install | a separate, lower budget |
| Request body size | 64 KB; larger returns 413 |
| Invalid tokens per IP address | a separate budget against guessing |
Exceeding a budget returns 429 with code rate_limited and a Retry-After header in seconds. Wait for it, then retry; never retry earlier.
Headers#
| Header | Direction | Meaning |
|---|---|---|
Authorization |
request | Bearer + the access token |
Idempotency-Key |
request | the safe-write key |
X-Request-Id |
response | a unique id per request; include it in any support report |
X-Dukkan-Api-Version |
response | the current dated revision of v1, today 2026-09-09 |
Idempotency-Replayed |
response | true when the response is a replay of a recorded result |
Retry-After |
response | seconds until the budget resets, with 429 |
Versioning#
v1in the path is the contract: no field is ever removed or retyped within it.- Additive changes (a new field, a new topic, a new enum value) ship as dated revisions that appear in
X-Dukkan-Api-Versionand are recorded in the changelog. - Webhook payloads carry the same revision in their header and
api_version: "v1"in the envelope. - Ignore fields you do not know, and never depend on field order.
Errors#
Every error carries the same envelope:
{
"error": {
"code": "conflict",
"message": "Order cannot be fulfilled in its current status",
"request_id": "7d0c2b1a-5e4f-4a3b-8c9d-0e1f2a3b4c5d"
}
}| Status | code |
When |
|---|---|---|
| 400 | invalid_request |
an invalid body or parameter, a missing Idempotency-Key, or a malformed cursor |
| 401 | unauthorized |
a missing, invalid, expired or revoked token |
| 403 | forbidden |
the required effective scope is missing |
| 404 | not_found |
the resource does not exist in the token-bound store |
| 409 | conflict |
a refused state transition, an inventory or refund conflict, or an Idempotency-Key conflict |
| 413 | payload_too_large |
a body over 64 KB |
| 429 | rate_limited |
the budget is exhausted; see Retry-After |
| 5xx | internal_error |
an internal error; the message is redacted, and request_id is what you report |
message is for humans and may change; branch on code and the status only. details is an optional object with validation specifics.
PII policy#
The API is customer-data free by default. The customer and shipping_address fields appear in GET /orders/{id} only when the install holds clients:read (gated by the data processing agreement), never in lists and never in any webhook. Every call is recorded in the merchant's audit trail with its status and request_id. See the scope reference and Data processing.
Money#
Every amount is a { amount_minor, currency, decimals } object. Read decimals from each amount; never use an ISO table. See Money in minor units.