{"document":{"openapi":"3.1.0","info":{"title":"Dukkan Platform API","version":"1.0.0","description":"PII-free app API. Money is integer minor units scaled by each Money object's own `decimals` field (Dukkan deviates from ISO 4217: SYP is zero-decimal — never scale with an ISO table). List endpoints use opaque cursors, every write requires Idempotency-Key, and tokens are budgeted at 180 requests/minute (60 writes/minute) — a 429 carries Retry-After. `components.schemas` and the `x-dukkan-*` extensions are generated from the zod contracts in shared/types/platform-api/v1 (`yarn gen:platform-openapi`); paths, parameters and responses are hand-written."},"servers":[{"url":"https://dukkan.one/platform-api/v1"}],"security":[{"bearerAuth":[]}],"tags":[{"name":"Installation","description":"Identity of the token's install and store"},{"name":"Store","description":"Public identity and checkout facts of the token's store"},{"name":"Orders"},{"name":"Products"},{"name":"Discounts"},{"name":"Inventory"},{"name":"Webhooks"}],"paths":{"/installation":{"get":{"tags":["Installation"],"operationId":"getInstallation","summary":"Identify the install and store behind this token","description":"Needs no scope. Returns the platform-issued ids an app must key its storage on (`install.id`, `store.id`), the effective scope set every request is checked against, the store's currency exponent, locale and timezone, and the current webhook subscription without its secret. `store.slug` is display-only and can change.","responses":{"200":{"description":"The install and its store","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Installation"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/store":{"get":{"tags":["Store"],"operationId":"getStore","summary":"Public identity and checkout facts of the store","description":"Needs no scope; everything here is already public on the storefront. Carries the pricing currency, the currencies a customer may pay in, the store-adjusted FX table checkout applies (integer `scaled / 10^scale`, base at identity), the enabled manual payment methods with the ShamCash id shown at checkout, the province vocabulary for `shipping_address.province`, and the public https origin (a connected custom domain wins).","responses":{"200":{"description":"The store profile","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Store"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/orders":{"get":{"tags":["Orders"],"operationId":"listOrders","x-dukkan-scope":"orders:read","summary":"List orders","parameters":[{"$ref":"#/components/parameters/Cursor"},{"$ref":"#/components/parameters/Limit"},{"name":"status","in":"query","schema":{"$ref":"#/components/schemas/OrderStatus"}},{"name":"created_at_min","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"updated_at_min","in":"query","description":"Incremental sync for status/payment changes (cursor stays keyed on created_at)","schema":{"type":"string","format":"date-time"}},{"name":"tag","in":"query","description":"Only orders carrying this tag (case-insensitive)","schema":{"type":"string","maxLength":40}}],"responses":{"200":{"description":"Cursor page of PII-free orders","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderPage"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["Orders"],"operationId":"createOrder","x-dukkan-scope":"orders:create","summary":"Create an order with the prices the merchant agreed","description":"Lines carry the agreed unit price (catalog variants or custom lines with no variant); totals are recomputed server-side (subtotal = Σ quantity × unit_price_minor, total = subtotal − discount + shipping, tax 0) and every side effect of a storefront order follows: stock decrement obeying policy (409 `insufficient_stock` with `details.lines[]` and nothing written), CRM link by phone, merchant push and email, `order.created` with `source: app`, and a timeline row naming the app. Specific codes: 422 `currency_not_allowed`, 404 `variant_not_found`, 409 `variant_unavailable`, 409 `payment_method_disabled`. The response echoes the customer details the app supplied regardless of `clients:read`.","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateOrderRequest"}}}},"responses":{"201":{"description":"Order created (or the recorded response on an idempotent replay)","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/OrderDetail"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/UnprocessableEntity"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/orders/{id}":{"get":{"tags":["Orders"],"operationId":"getOrder","x-dukkan-scope":"orders:read","summary":"Get an order and immutable line snapshots","description":"`customer` and `shipping_address` are present ONLY when the install holds the DPA-gated `clients:read` scope; without it the keys are absent, not null.","parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"Order detail","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/OrderDetail"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"patch":{"tags":["Orders"],"operationId":"updateOrder","x-dukkan-scope":"orders:write","summary":"Update the note, add or remove tags, or merge the app's own attributes","description":"Money, lines, customer and address are immutable after creation. Tags are set operations (add/remove) so the merchant's and other apps' tags survive. `attributes` is the calling app's private namespace; `null` deletes a key; `reference.url` must be https on the app's registered origin.","parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateOrderRequest"}}}},"responses":{"200":{"description":"The updated order","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/OrderDetail"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/UnprocessableEntity"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/orders/{id}/status":{"patch":{"tags":["Orders"],"operationId":"updateOrderStatus","x-dukkan-scope":"orders:write","summary":"Transition order status through the server matrix","description":"Money-bearing statuses (completed, partially_refunded, refunded) are reachable only through the refund writers. A transition the matrix does not allow answers 409 with `current_status` in the details.","parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateOrderStatusRequest"}}}},"responses":{"200":{"description":"Status transitioned"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/orders/{id}/fulfillments":{"post":{"tags":["Orders"],"operationId":"createFulfillment","x-dukkan-scope":"fulfillments:write","summary":"Fulfill selected line quantities; ship the order once every line is fulfilled","description":"Quantities above the still-unfulfilled amount of a line answer 409. Emits `fulfillment.created`, plus `fulfillment.requested` when the fulfillment is created as `pending` (a pickup waiting to be booked).","parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateFulfillmentRequest"}}}},"responses":{"201":{"description":"Fulfillment created; the order auto-ships only when every line is fulfilled AND the fulfillment was created as \"shipped\""},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/orders/{id}/fulfillments/{fulfillmentId}":{"patch":{"tags":["Orders"],"operationId":"updateFulfillment","x-dukkan-scope":"fulfillments:write","summary":"Update tracking, mark delivered, or cancel a fulfillment","description":"Identity and lines are immutable. Legal transitions are pending → shipped → delivered and pending|shipped → cancelled. Cancelling releases the quantity the fulfillment consumed, so the line can be fulfilled again. Terminal (delivered/cancelled) fulfillments reject further edits with 409.","parameters":[{"$ref":"#/components/parameters/Id"},{"name":"fulfillmentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateFulfillmentRequest"}}}},"responses":{"200":{"description":"Fulfillment updated. When the last outstanding fulfillment is delivered and every line is covered, the order transitions to \"delivered\" (order_status: \"delivered\" in the response); order_status_skipped is true when that transition was refused by the order's current state. COD payment status is untouched — order.paid rides the payments ledger."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/orders/{id}/refunds":{"post":{"tags":["Orders"],"operationId":"createRefund","x-dukkan-scope":"refunds:write","summary":"Append a refund against captured funds","description":"`amount_minor` may not exceed paid minus already refunded. POS orders are refunded at the point of sale and answer 409 here. `restock` additionally requires the inventory:write scope.","parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRefundRequest"}}}},"responses":{"201":{"description":"Refund created. Refundability is gated on remaining captured funds, not order status; when the status matrix has no refund edge (e.g. a terminal `cancelled`) the money still moves and `order_status_skipped` reports that the status was left untouched."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/webhooks":{"get":{"tags":["Webhooks"],"operationId":"getWebhookSubscription","summary":"Read this install's webhook subscription","description":"The signing secret is never returned; it can only be rotated.","responses":{"200":{"description":"The active subscription, or null when none is configured","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/WebhookSubscription"}]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}},"put":{"tags":["Webhooks"],"operationId":"upsertWebhookSubscription","summary":"Create or replace this install's webhook subscription","description":"One endpoint per install; a PUT fully replaces the topic list. The signing secret is returned EXACTLY ONCE — on creation, or when rotate_secret is true — and can never be read back afterwards. Topics are scope-gated: subscribing to a topic requires the read scope that governs its payload. endpoint_url must be a public HTTPS URL.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertWebhookSubscriptionRequest"}}}},"responses":{"200":{"description":"Subscription created or replaced; `secret` present only when minted","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/WebhookSubscription"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"tags":["Webhooks"],"operationId":"deleteWebhookSubscription","summary":"Stop delivery for this install","responses":{"204":{"description":"Subscription revoked"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/webhooks/test":{"post":{"tags":["Webhooks"],"operationId":"createWebhookTestEvent","summary":"Fire one signed test delivery (sandbox stores only)","description":"Writes a real outbox event for the topic, built from the sandbox store's own records (overridable through `data`), fans it out to this install's subscription and lets the delivery worker sign and send it with the same secret and SSRF checks as production traffic. The envelope carries `test: true`. Requires an active subscription that includes the topic and the scope that governs it. Budget: 30 per ten minutes per install. Answers 403 `sandbox_required` on real stores, 409 `webhook_not_subscribed` / `webhook_topic_not_subscribed` / `sample_unavailable` when the rehearsal cannot be built.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWebhookTestEventRequest"}}}},"responses":{"202":{"description":"Event written and fanned out; the worker sends within about a minute","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/WebhookTestEvent"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/Conflict"},"413":{"description":"Override data exceeds 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/products":{"get":{"tags":["Products"],"operationId":"listProducts","x-dukkan-scope":"products:read","summary":"List or search products and variants","description":"With `q` the page is ranked by similarity and carries no cursor (at most `limit` results); without it the cursor pages newest first. `available_quantity` is null unless the install also holds inventory:read.","parameters":[{"$ref":"#/components/parameters/Cursor"},{"$ref":"#/components/parameters/Limit"},{"name":"updated_at_min","in":"query","description":"Incremental sync filter (the cursor itself keys on immutable created_at)","schema":{"type":"string","format":"date-time"}},{"name":"q","in":"query","description":"Case-insensitive substring match on product name, variant name, SKU, DSIN and barcode","schema":{"type":"string","minLength":2,"maxLength":80}},{"name":"status","in":"query","schema":{"type":"string","enum":["active","out_of_stock","hidden","discontinued","no_variants"]}},{"name":"ids","in":"query","description":"Comma-separated product ids (at most 100)","schema":{"type":"string"}}],"responses":{"200":{"description":"Product page","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductPage"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/products/{id}":{"get":{"tags":["Products"],"operationId":"getProduct","x-dukkan-scope":"products:read","summary":"Get one product with its variants","parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"Product detail","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Product"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/products/variants":{"get":{"tags":["Products"],"operationId":"listVariants","x-dukkan-scope":"products:read","summary":"Batch variant lookup by id","description":"Re-validate quoted lines before sending an offer or creating an order. Unknown ids are omitted, never errors. `available_quantity` needs inventory:read.","parameters":[{"name":"ids","in":"query","required":true,"description":"Comma-separated variant ids (at most 100)","schema":{"type":"string"}}],"responses":{"200":{"description":"The variants that exist in the store","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VariantList"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/discounts":{"get":{"tags":["Discounts"],"operationId":"listDiscounts","x-dukkan-scope":"discounts:read","summary":"List discounts (read-only)","description":"Requires the discounts:read scope. Discounts are read-only for apps: redemption accounting stays first-party, `used_count` is the app-visible redemption summary.","parameters":[{"$ref":"#/components/parameters/Cursor"},{"$ref":"#/components/parameters/Limit"}],"responses":{"200":{"description":"Discount page","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DiscountPage"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/inventory/movements":{"get":{"tags":["Inventory"],"operationId":"listInventoryMovements","x-dukkan-scope":"inventory:read","summary":"List append-only inventory movements","parameters":[{"$ref":"#/components/parameters/Cursor"},{"$ref":"#/components/parameters/Limit"}],"responses":{"200":{"description":"Movement page","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MovementPage"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["Inventory"],"operationId":"createInventoryMovement","x-dukkan-scope":"inventory:write","summary":"Append an inventory movement; absolute stock writes are forbidden","description":"The response never includes the resulting absolute stock; read the movement list or the product for that.","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInventoryMovementRequest"}}}},"responses":{"201":{"description":"Movement appended"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"}}}}},"webhooks":{"storeEvent":{"post":{"operationId":"receiveStoreEvent","summary":"Signed store event delivery (at-least-once)","description":"Delivered for every subscribed topic. Verify `X-Dukkan-Hmac-Sha256` (hex HMAC-SHA256 of `{X-Dukkan-Timestamp}.{raw body}` with the signing secret of the install named in the signed `install_id`), reject timestamps older than five minutes, deduplicate on the signed envelope `id`, and refuse a `store_id` that differs from the install you hold. `sequence` is strictly increasing per store with gaps — order by it, never count on contiguity. Topics require the matching granted scope (see `x-dukkan-topic-scopes`); `app.uninstalled` needs no scope and still delivers after revocation. Respond 2xx with a small body within the timeout; anything else is retried with exponential backoff up to 8 attempts, after which the delivery is dead-lettered and sustained failures disable the subscription. Sandbox rehearsals fired through POST /webhooks/test carry `test: true`.","parameters":[{"name":"X-Dukkan-Delivery-Id","in":"header","required":true,"schema":{"type":"string","format":"uuid"},"description":"Delivery attempt id (unsigned; retries reuse it)"},{"name":"X-Dukkan-Install-Id","in":"header","required":true,"schema":{"type":"string","format":"uuid"},"description":"Unsigned hint of the signed envelope install_id","for secret lookup before verification":null},{"name":"X-Dukkan-Event","in":"header","required":true,"schema":{"$ref":"#/components/schemas/WebhookTopic"}},{"name":"X-Dukkan-Timestamp","in":"header","required":true,"schema":{"type":"string","description":"Unix seconds"}},{"name":"X-Dukkan-Api-Version","in":"header","required":true,"schema":{"type":"string","description":"Dated v1 revision that produced this payload"}},{"name":"X-Dukkan-Hmac-Sha256","in":"header","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEnvelope"}}}},"responses":{"200":{"description":"Acknowledged — any 2xx stops retries"}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer"}},"parameters":{"Id":{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},"Cursor":{"name":"cursor","in":"query","schema":{"type":"string","maxLength":512}},"Limit":{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":true,"description":"Unique per logical write, at most 200 characters, remembered for 24 hours. Replaying the same key with the same body returns the recorded response; the same key with a different body answers 409.","schema":{"type":"string","minLength":1,"maxLength":200}}},"responses":{"BadRequest":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unauthorized":{"description":"Missing, invalid, expired, or revoked token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Forbidden":{"description":"Required live scope is missing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"Resource not found in the token-bound store","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"UnprocessableEntity":{"description":"Well-formed body that fails the contract; `details.fields[]` names the paths","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Conflict":{"description":"State transition, inventory, refund, or idempotency conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"Token budget exhausted (180 requests/minute, 60 writes/minute per install)","headers":{"Retry-After":{"schema":{"type":"integer","description":"Seconds until the budget resets"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Stable machine code: invalid_request, unauthorized, forbidden, not_found, conflict, payload_too_large, rate_limited, internal_error, or a more specific code named on the operation."},"message":{"type":"string"},"request_id":{"description":"Echo of X-Request-Id; quote it when reporting a problem.","type":"string"},"details":{"type":"object","additionalProperties":true}},"required":["code","message"]}},"required":["error"],"description":"Every non-2xx response. 5xx bodies carry only `internal_error` and the request id."},"Money":{"type":"object","properties":{"amount_minor":{"type":"integer"},"currency":{"type":"string","minLength":3,"maxLength":3},"decimals":{"type":"integer","minimum":0,"maximum":4,"description":"Minor-unit exponent for THIS amount. Dukkan deviates from ISO 4217 (SYP is zero-decimal), so always scale by this field, never by an ISO currency table."}},"required":["amount_minor","currency","decimals"],"description":"Integer minor units plus the exponent that scales them. Never a float."},"OrderStatus":{"type":"string","enum":["placed","approved","processing","shipped","delivered","delivered_failed","returned","cancelled","completed","partially_refunded","refunded"],"description":"Order lifecycle state. Transitions follow the server matrix; money-bearing states are reachable only through refunds."},"SettableOrderStatus":{"type":"string","enum":["placed","approved","processing","shipped","delivered","delivered_failed","returned","cancelled"],"description":"Statuses an app may set through PATCH /orders/{id}/status. Money-bearing statuses are reachable only through refunds."},"PaymentStatus":{"type":"string","enum":["not_required","unpaid","pending","authorized","partially_paid","paid","partially_refunded","refunded","failed"],"description":"Ledger-projected payment state. Independent of the delivery status: a delivered COD order is `unpaid` until the remittance is confirmed."},"WebhookTopic":{"type":"string","enum":["order.created","order.status_changed","order.paid","product.created","product.updated","product.deleted","inventory.movement_created","fulfillment.requested","fulfillment.created","fulfillment.updated","refund.created","app.uninstalled"],"description":"Webhook topic. Subscribing needs the read scope that governs the payload; `app.uninstalled` needs none and still delivers after revocation."},"WebhookEnvelope":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Event id. Deduplicate on this signed value, never on the delivery header."},"api_version":{"type":"string","const":"v1"},"topic":{"$ref":"#/components/schemas/WebhookTopic"},"store_id":{"type":"string","format":"uuid"},"install_id":{"type":"string","format":"uuid","description":"The install this delivery was fanned out to. Verify the signature with THIS install's secret and refuse a store mismatch."},"sequence":{"type":"integer","exclusiveMinimum":0,"description":"Strictly increasing per store with gaps. Order by it; never count on contiguity."},"occurred_at":{"type":"string","format":"date-time"},"test":{"description":"Present and true only for sandbox test deliveries (POST /webhooks/test). Absent in production traffic.","type":"boolean"},"data":{"type":"object","additionalProperties":true}},"required":["id","api_version","topic","store_id","install_id","sequence","occurred_at","data"],"description":"Signed store event. Verify X-Dukkan-Hmac-Sha256 over `{X-Dukkan-Timestamp}.{raw body}` with the install's signing secret before parsing."},"Order":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"order_number":{"type":"string"},"status":{"$ref":"#/components/schemas/OrderStatus"},"payment_status":{"$ref":"#/components/schemas/PaymentStatus"},"source":{"type":["string","null"],"description":"Sales channel that created the order (`storefront`, `pos`, `admin`, ...)."},"line_pricing":{"type":"string","enum":["net","gross"],"description":"How line totals relate to order totals. \"gross\": sum of line totals equals subtotal and the discount lives on the order. \"net\" (POS orders): line totals are net of the applied promo, so sum of line totals plus discount equals subtotal. Branch on this field when reconciling, never on `source`."},"subtotal":{"$ref":"#/components/schemas/Money"},"discount":{"$ref":"#/components/schemas/Money"},"shipping":{"$ref":"#/components/schemas/Money"},"tax":{"$ref":"#/components/schemas/Money"},"total":{"$ref":"#/components/schemas/Money"},"paid":{"$ref":"#/components/schemas/Money"},"refunded":{"$ref":"#/components/schemas/Money"},"exchange_rate":{"type":["object","null"],"description":"Snapshot of the rate applied when the order was priced in a non-store currency: rate = scaled / 10^scale.","properties":{"scaled":{"type":"integer"},"scale":{"type":"integer","minimum":0,"maximum":12}},"required":["scaled","scale"]},"tags":{"type":"array","items":{"type":"string"},"description":"Merchant and app tags (see PATCH /orders/{id})."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","order_number","status","payment_status","source","line_pricing","subtotal","discount","shipping","tax","total","paid","refunded","exchange_rate","tags","created_at","updated_at"],"description":"PII-free order. Customer contact fields appear only on the detail endpoint with the clients:read scope."},"OrderItem":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"variant_id":{"type":["string","null"],"format":"uuid","description":"Null for custom lines."},"kind":{"type":"string","enum":["variant","custom"],"description":"`custom` lines have no catalog link: they never touch inventory and refund by amount only."},"name":{"type":"string","description":"Product name snapshot at order time."},"quantity":{"type":"integer","exclusiveMinimum":0},"unit_price":{"$ref":"#/components/schemas/Money"},"total":{"$ref":"#/components/schemas/Money"}},"required":["id","variant_id","kind","name","quantity","unit_price","total"],"description":"Immutable line snapshot."},"OrderCustomer":{"type":"object","properties":{"first_name":{"type":["string","null"]},"last_name":{"type":["string","null"]},"email":{"type":["string","null"]},"phone":{"type":["string","null"]}},"required":["first_name","last_name","email","phone"],"description":"Customer contact details. Requires the `clients:read` scope."},"FulfillmentLine":{"type":"object","properties":{"order_item_id":{"type":"string","format":"uuid"},"quantity":{"type":"integer","exclusiveMinimum":0}},"required":["order_item_id","quantity"]},"Fulfillment":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"provider":{"type":["string","null"]},"tracking_number":{"type":["string","null"]},"status":{"type":"string","enum":["pending","shipped","delivered","cancelled"]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"lines":{"type":"array","items":{"$ref":"#/components/schemas/FulfillmentLine"}}},"required":["id","provider","tracking_number","status","created_at","updated_at","lines"],"description":"A shipment covering some or all lines of an order."},"RefundLine":{"type":"object","properties":{"order_item_id":{"type":["string","null"],"format":"uuid"},"quantity":{"type":"integer","exclusiveMinimum":0},"amount_minor":{"type":"integer"}},"required":["order_item_id","quantity","amount_minor"]},"Refund":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"amount":{"$ref":"#/components/schemas/Money"},"reason":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"lines":{"type":"array","items":{"$ref":"#/components/schemas/RefundLine"}}},"required":["id","amount","reason","created_at","lines"],"description":"An append-only refund against captured funds."},"OrderReference":{"type":"object","properties":{"kind":{"type":"string","minLength":1,"maxLength":40,"description":"Record type in the app, e.g. `quote`."},"number":{"type":"string","minLength":1,"maxLength":80,"description":"Human-readable record number, e.g. `Q-1042`."},"url":{"type":["string","null"],"format":"uri","description":"Deep link into the app. Must be https on the app's registered origin; otherwise 422.","maxLength":2000,"default":null}},"required":["kind","number"],"additionalProperties":false,"description":"Link from an order back to the app's own record."},"OrderAttributes":{"type":"object","propertyNames":{"type":"string","pattern":"^[a-z0-9_.-]{1,64}$"},"additionalProperties":{"anyOf":[{"anyOf":[{"type":"string","maxLength":1000},{"type":"number"},{"type":"boolean"},{"type":"null"}]},{"$ref":"#/components/schemas/OrderReference"}],"description":"A scalar, or the `reference` object under the `reference` key."},"description":"Up to 20 keys (`[a-z0-9_.-]{1,64}`), scalar values up to 1000 UTF-8 bytes, 8192 bytes in total. Private to the calling app."},"OrderAttributesPatch":{"type":"object","propertyNames":{"type":"string","pattern":"^[a-z0-9_.-]{1,64}$"},"additionalProperties":{"anyOf":[{"anyOf":[{"type":"string","maxLength":1000},{"type":"number"},{"type":"boolean"},{"type":"null"}]},{"$ref":"#/components/schemas/OrderReference"}],"description":"A scalar, or the `reference` object under the `reference` key."},"description":"Merged into the app's namespace; a `null` value deletes that key."},"OrderDetail":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"order_number":{"type":"string"},"status":{"$ref":"#/components/schemas/OrderStatus"},"payment_status":{"$ref":"#/components/schemas/PaymentStatus"},"source":{"type":["string","null"],"description":"Sales channel that created the order (`storefront`, `pos`, `admin`, ...)."},"line_pricing":{"type":"string","enum":["net","gross"],"description":"How line totals relate to order totals. \"gross\": sum of line totals equals subtotal and the discount lives on the order. \"net\" (POS orders): line totals are net of the applied promo, so sum of line totals plus discount equals subtotal. Branch on this field when reconciling, never on `source`."},"subtotal":{"$ref":"#/components/schemas/Money"},"discount":{"$ref":"#/components/schemas/Money"},"shipping":{"$ref":"#/components/schemas/Money"},"tax":{"$ref":"#/components/schemas/Money"},"total":{"$ref":"#/components/schemas/Money"},"paid":{"$ref":"#/components/schemas/Money"},"refunded":{"$ref":"#/components/schemas/Money"},"exchange_rate":{"type":["object","null"],"description":"Snapshot of the rate applied when the order was priced in a non-store currency: rate = scaled / 10^scale.","properties":{"scaled":{"type":"integer"},"scale":{"type":"integer","minimum":0,"maximum":12}},"required":["scaled","scale"]},"tags":{"type":"array","items":{"type":"string"},"description":"Merchant and app tags (see PATCH /orders/{id})."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"items":{"type":"array","items":{"$ref":"#/components/schemas/OrderItem"}},"fulfillments":{"type":"array","items":{"$ref":"#/components/schemas/Fulfillment"}},"refunds":{"type":"array","items":{"$ref":"#/components/schemas/Refund"}},"note":{"type":["string","null"],"description":"Order note (merchant or app)."},"attributes":{"description":"The CALLING app's own attributes on this order. Other apps' namespaces are never returned.","$ref":"#/components/schemas/OrderAttributes"},"customer":{"description":"Present only when the install holds the `clients:read` scope (customer-PII scope, DPA-gated). Absent entirely otherwise.","$ref":"#/components/schemas/OrderCustomer"},"shipping_address":{"type":["object","null"],"description":"Free-form delivery address snapshot. Present only with the `clients:read` scope; absent entirely otherwise. May be null when the order has no delivery address (e.g. POS sales).","additionalProperties":true}},"required":["id","order_number","status","payment_status","source","line_pricing","subtotal","discount","shipping","tax","total","paid","refunded","exchange_rate","tags","created_at","updated_at","items","fulfillments","refunds","note","attributes"],"description":"Order with its immutable line snapshots and reconciliation collections."},"Variant":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"product_id":{"type":"string","format":"uuid"},"sku":{"type":["string","null"]},"price":{"$ref":"#/components/schemas/Money"},"status":{"type":"string"},"name":{"type":"string","description":"Variant label (e.g. `أحمر / L`)."},"attributes":{"type":"object","additionalProperties":{"type":"string"},"description":"Option name → value, e.g. { color: 'أحمر', size: 'L' }."},"barcode":{"type":["string","null"]},"compare_at_price":{"description":"Higher strike-through price when set.","oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Money"}]},"image_url":{"type":["string","null"],"description":"Main image for this variant (own image, else the product's)."},"weight_gram":{"type":["integer","null"]},"available_quantity":{"type":["integer","null"],"description":"Stock minus reservations across the store's locations. Null when the product is not inventory-managed or the install lacks inventory:read."}},"required":["id","product_id","sku","price","status","name","attributes","barcode","compare_at_price","image_url","weight_gram","available_quantity"]},"VariantDetail":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"product_id":{"type":"string","format":"uuid"},"sku":{"type":["string","null"]},"price":{"$ref":"#/components/schemas/Money"},"status":{"type":"string"},"name":{"type":"string","description":"Variant label (e.g. `أحمر / L`)."},"attributes":{"type":"object","additionalProperties":{"type":"string"},"description":"Option name → value, e.g. { color: 'أحمر', size: 'L' }."},"barcode":{"type":["string","null"]},"compare_at_price":{"description":"Higher strike-through price when set.","oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Money"}]},"image_url":{"type":["string","null"],"description":"Main image for this variant (own image, else the product's)."},"weight_gram":{"type":["integer","null"]},"available_quantity":{"type":["integer","null"],"description":"Stock minus reservations across the store's locations. Null when the product is not inventory-managed or the install lacks inventory:read."},"product_name":{"type":"string"},"product_status":{"type":"string"}},"required":["id","product_id","sku","price","status","name","attributes","barcode","compare_at_price","image_url","weight_gram","available_quantity","product_name","product_status"],"description":"Variant with its product's name and status (GET /products/variants)."},"VariantList":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/VariantDetail"}}},"required":["data"],"description":"Batch variant lookup. Unknown ids are omitted, never errors."},"ProductCategory":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"}},"required":["id","name"]},"Product":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"dsin":{"type":"string","description":"Dukkan store item number: the merchant-facing product code."},"name":{"type":"string"},"status":{"type":"string"},"updated_at":{"type":["string","null"],"format":"date-time"},"image_url":{"type":["string","null"],"description":"Main product image."},"image_color":{"type":["string","null"],"description":"Dominant colour of the main image (#rrggbb) for placeholders."},"brand":{"type":["string","null"]},"category":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/ProductCategory"}]},"description":{"type":["string","null"],"description":"Plain text, at most 500 characters."},"images":{"description":"Gallery, present on GET /products/{id} only.","type":"array","items":{"type":"string"}},"variants":{"type":"array","items":{"$ref":"#/components/schemas/Variant"}}},"required":["id","dsin","name","status","updated_at","image_url","image_color","brand","category","description","variants"]},"Store":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"logo_url":{"type":["string","null"]},"banner_urls":{"type":"array","items":{"type":"string"}},"storefront_url":{"type":"string","description":"Public https origin; a connected custom domain wins over the platform subdomain."},"whatsapp_number":{"type":["string","null"]},"phone":{"type":["string","null"]},"address":{"type":["string","null"]},"accent_color":{"type":["string","null"],"description":"Published theme accent, or the store's primary colour."},"locale":{"type":"string"},"timezone":{"type":"string"},"country":{"type":["string","null"]},"pricing_currency":{"type":"object","properties":{"code":{"type":"string","minLength":3,"maxLength":3},"decimals":{"type":"integer","minimum":0,"maximum":4}},"required":["code","decimals"],"description":"The currency catalog prices are stored in."},"storefront_currencies":{"type":"object","properties":{"allowed":{"type":"array","items":{"type":"string","minLength":3,"maxLength":3}},"default":{"type":"string","minLength":3,"maxLength":3}},"required":["allowed","default"],"description":"Currencies a customer may pay in; POST /orders accepts these only."},"fx":{"type":["object","null"],"description":"Store-adjusted rates from the pricing currency, exactly what checkout applies; includes the base at identity. Null when no rate table is available.","properties":{"base":{"type":"string","minLength":3,"maxLength":3},"as_of":{"type":"string","format":"date-time"},"rates":{"type":"object","additionalProperties":{"type":"object","properties":{"scaled":{"type":"integer","exclusiveMinimum":0},"scale":{"type":"integer","minimum":0,"maximum":12}},"required":["scaled","scale"],"description":"rate = scaled / 10^scale."}}},"required":["base","as_of","rates"]},"payment_methods":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","enum":["cod","shamcash_manual"]},"shamcash_id":{"description":"Present for shamcash_manual: the pay-to id shown at checkout.","type":"string"}},"required":["id"]},"description":"Enabled manual payment methods."},"provinces":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string"},"ar":{"type":"string"},"en":{"type":"string"}},"required":["value","ar","en"]},"description":"Accepted `shipping_address.province` values with labels."}},"required":["id","slug","name","description","logo_url","banner_urls","storefront_url","whatsapp_number","phone","address","accent_color","locale","timezone","country","pricing_currency","storefront_currencies","fx","payment_methods","provinces"],"description":"Public identity and checkout facts of the token's store."},"Discount":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"method":{"type":"string","enum":["code","automatic"]},"code":{"type":["string","null"]},"title":{"type":["string","null"]},"value_type":{"type":"string","enum":["percentage","fixed_amount","free_shipping","buy_x_get_y"]},"target":{"type":"string","enum":["order","products","categories"]},"percent":{"type":["number","null"],"description":"Set only for percentage discounts; `amount` only for fixed_amount."},"amount":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Money"}]},"is_active":{"type":"boolean"},"usage_limit_total":{"type":["integer","null"]},"used_count":{"type":"integer","minimum":0},"applies_to_pos":{"type":"boolean"},"applies_to_online":{"type":"boolean"},"starts_at":{"type":["string","null"],"format":"date-time"},"ends_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":["string","null"],"format":"date-time"}},"required":["id","method","code","title","value_type","target","percent","amount","is_active","usage_limit_total","used_count","applies_to_pos","applies_to_online","starts_at","ends_at","created_at","updated_at"],"description":"Read-only view of a discount. Redemption accounting stays first-party."},"Movement":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"sequence":{"type":"integer","minimum":0,"description":"Monotonic insert sequence; the stable sort key for what happened in which order."},"variant_id":{"type":["string","null"],"format":"uuid"},"location_id":{"type":"string","format":"uuid"},"delta":{"type":"integer"},"stock_after":{"type":"integer"},"reason":{"type":"string"},"occurred_at":{"type":"string","format":"date-time"}},"required":["id","sequence","variant_id","location_id","delta","stock_after","reason","occurred_at"],"description":"One append-only stock movement."},"WebhookSubscription":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"endpoint_url":{"type":"string"},"topics":{"type":"array","items":{"$ref":"#/components/schemas/WebhookTopic"}},"status":{"type":"string","enum":["active","disabled"],"description":"`disabled` = quarantined after sustained delivery failures; re-PUT the subscription to resume."},"consecutive_failures":{"type":"integer","minimum":0},"activated_at":{"type":"string","format":"date-time","description":"Events that occurred before this instant are never delivered to this subscription."},"disabled_at":{"type":["string","null"],"format":"date-time"},"secret":{"description":"Present only on the PUT response that minted or rotated it. Store it immediately; it can never be read back.","type":"string"}},"required":["id","endpoint_url","topics","status","consecutive_failures","activated_at","disabled_at"],"description":"One subscription per install; PUT is a full replace."},"Installation":{"type":"object","properties":{"install":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"app_id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["active","suspended"],"description":"A token only resolves for an active install; `suspended` is reserved for billing holds surfaced through this document."},"granted_scopes":{"type":"array","items":{"type":"string"},"description":"Scopes the merchant consented to."},"effective_scopes":{"type":"array","items":{"type":"string"},"description":"Granted scopes intersected with the grantor's live permissions. This is what every request is checked against."},"distribution_channel":{"type":"string","enum":["development","private","public"]},"installed_at":{"type":"string","format":"date-time"},"api_version":{"type":"string","description":"Dated v1 revision this server speaks (same value as X-Dukkan-Api-Version)."},"webhook":{"type":["object","null"],"description":"The active subscription, or null when none is configured.","properties":{"id":{"type":"string","format":"uuid"},"endpoint_url":{"type":"string"},"topics":{"type":"array","items":{"$ref":"#/components/schemas/WebhookTopic"}},"status":{"type":"string","enum":["active","disabled"],"description":"`disabled` = quarantined after sustained delivery failures; re-PUT the subscription to resume."},"consecutive_failures":{"type":"integer","minimum":0},"activated_at":{"type":"string","format":"date-time","description":"Events that occurred before this instant are never delivered to this subscription."},"disabled_at":{"type":["string","null"],"format":"date-time"}},"required":["id","endpoint_url","topics","status","consecutive_failures","activated_at","disabled_at"]}},"required":["id","app_id","status","granted_scopes","effective_scopes","distribution_channel","installed_at","api_version","webhook"]},"store":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"},"currency":{"type":"string","minLength":3,"maxLength":3},"decimals":{"type":"integer","minimum":0,"maximum":4,"description":"Minor-unit exponent of the store currency."},"locale":{"type":"string","description":"Storefront locale (`ar` or `en`)."},"timezone":{"type":"string","description":"IANA zone derived from the store country; use it for day boundaries in reports."},"country":{"type":["string","null"],"description":"ISO 3166-1 alpha-2 when the merchant set one."},"is_development":{"type":"boolean","description":"True for sandbox stores. Test deliveries and dev sessions exist only here."}},"required":["id","slug","name","currency","decimals","locale","timezone","country","is_development"]}},"required":["install","store"],"description":"The install and the store it is bound to, as the platform sees them."},"WebhookTestEvent":{"type":"object","properties":{"event_id":{"type":"string","format":"uuid"},"topic":{"$ref":"#/components/schemas/WebhookTopic"},"sequence":{"type":"integer","exclusiveMinimum":0},"occurred_at":{"type":"string","format":"date-time"},"test":{"type":"boolean","const":true},"data":{"type":"object","additionalProperties":true,"description":"The payload that was written, after overrides and validation."},"deliveries":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Delivery id; also sent as X-Dukkan-Delivery-Id."},"status":{"type":"string","enum":["pending","delivering","retry_scheduled","succeeded","dead"]},"endpoint_url":{"type":"string"}},"required":["id","status","endpoint_url"]},"description":"One row per subscription the event fanned out to; the worker sends within about a minute."}},"required":["event_id","topic","sequence","occurred_at","test","data","deliveries"],"description":"The accepted test event and its queued deliveries."},"OrderPage":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Order"}},"next_cursor":{"type":["string","null"],"description":"Pass back as `cursor` to fetch the next page; null on the last page."}},"required":["data","next_cursor"],"description":"Cursor page of PII-free orders."},"ProductPage":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Product"}},"next_cursor":{"type":["string","null"],"description":"Pass back as `cursor` to fetch the next page; null on the last page."}},"required":["data","next_cursor"],"description":"Cursor page of products with variants."},"DiscountPage":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Discount"}},"next_cursor":{"type":["string","null"],"description":"Pass back as `cursor` to fetch the next page; null on the last page."}},"required":["data","next_cursor"],"description":"Cursor page of discounts."},"MovementPage":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Movement"}},"next_cursor":{"type":["string","null"],"description":"Pass back as `cursor` to fetch the next page; null on the last page."}},"required":["data","next_cursor"],"description":"Cursor page of append-only inventory movements."},"CreateOrderItem":{"type":"object","properties":{"variant_id":{"type":["string","null"],"format":"uuid","description":"Catalog variant, or null for a custom line."},"name":{"description":"Line label. Required when variant_id is null; ignored otherwise (the catalog name is snapshotted).","type":"string","minLength":1,"maxLength":200},"quantity":{"type":"integer","minimum":1,"maximum":100000},"unit_price_minor":{"type":"integer","minimum":0,"maximum":10000000000000,"description":"Agreed unit price in minor units of the order currency. Zero is allowed."}},"required":["variant_id","quantity","unit_price_minor"],"additionalProperties":false,"description":"One order line with the price the merchant agreed."},"CreateOrderRequest":{"type":"object","properties":{"currency":{"description":"One of the store's allowed storefront currencies; otherwise 422 `currency_not_allowed`.","type":"string","minLength":3,"maxLength":3},"status":{"default":"placed","description":"`placed` (default) lets the merchant review; `approved` records an explicit customer acceptance the app collected.","type":"string","enum":["placed","approved"]},"items":{"minItems":1,"maxItems":200,"type":"array","items":{"$ref":"#/components/schemas/CreateOrderItem"}},"discount":{"type":["object","null"],"description":"Order-level discount; must not exceed the sum of the lines.","properties":{"amount_minor":{"type":"integer"},"title":{"type":["string","null"],"maxLength":80,"default":null}},"required":["amount_minor"],"additionalProperties":false,"default":null},"shipping":{"type":"object","properties":{"amount_minor":{"type":"integer"},"method":{"type":"string","enum":["delivery","pickup"]}},"required":["amount_minor","method"],"additionalProperties":false},"customer":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"phone":{"type":"string","minLength":5,"maxLength":32,"description":"Full international number with country code; stored normalised."},"email":{"type":["string","null"],"format":"email","description":"Optional. Never invented when absent.","maxLength":254,"default":null}},"required":["name","phone"],"additionalProperties":false},"shipping_address":{"type":["object","null"],"description":"Required when shipping.method is `delivery`.","properties":{"address":{"type":"string","minLength":1,"maxLength":500},"province":{"type":"string","maxLength":40,"description":"Syrian province value, e.g. `damascus` (see GET /store `provinces`)."},"city":{"type":["string","null"],"maxLength":80,"default":null},"notes":{"type":["string","null"],"maxLength":500,"default":null}},"required":["address","province"],"additionalProperties":false,"default":null},"payment_method":{"type":"string","enum":["cod","shamcash_manual"],"description":"Must be enabled in the store; otherwise 409 `payment_method_disabled`."},"exchange_rate":{"type":["object","null"],"description":"Required when currency differs from the store's pricing currency, forbidden otherwise. rate = scaled / 10^scale, oriented 1 pricing-currency unit = rate order-currency units (same as GET /store `fx`).","properties":{"scaled":{"type":"integer","exclusiveMinimum":0},"scale":{"type":"integer","minimum":0,"maximum":12}},"required":["scaled","scale"],"additionalProperties":false,"default":null},"note":{"type":["string","null"],"maxLength":2000,"default":null},"tags":{"description":"At most 10 tags after case-insensitive dedupe.","maxItems":10,"type":"array","items":{"description":"Free-text tag: Unicode letters, marks, digits, space, _ and -, 1 to 40 characters. Normalised (NFC, trimmed) and deduped case-insensitively.","type":"string","maxLength":200}},"attributes":{"default":{},"$ref":"#/components/schemas/OrderAttributes"},"inventory":{"default":"decrement","type":"string","enum":["decrement","skip"]}},"required":["currency","items","shipping","customer","payment_method"],"additionalProperties":false,"description":"Create an order with app-set prices. Totals are recomputed server-side: subtotal = Σ quantity × unit_price_minor, total = subtotal − discount + shipping, tax 0."},"UpdateOrderRequest":{"type":"object","properties":{"note":{"type":["string","null"],"description":"Replace the order note; null clears it.","maxLength":2000},"tags":{"description":"Set operations, never a replace: the merchant's and other apps' tags survive.","type":"object","properties":{"add":{"description":"At most 10 tags after case-insensitive dedupe.","maxItems":10,"type":"array","items":{"description":"Free-text tag: Unicode letters, marks, digits, space, _ and -, 1 to 40 characters. Normalised (NFC, trimmed) and deduped case-insensitively.","type":"string","maxLength":200}},"remove":{"description":"Removed case-insensitively.","maxItems":10,"type":"array","items":{"type":"string","maxLength":200}}},"additionalProperties":false},"attributes":{"$ref":"#/components/schemas/OrderAttributesPatch"}},"additionalProperties":false,"minProperties":1,"description":"Update the note, add/remove tags, or merge the app's own attributes. Money, lines, customer and address are immutable after creation."},"UpdateOrderStatusRequest":{"type":"object","properties":{"status":{"$ref":"#/components/schemas/SettableOrderStatus"}},"required":["status"],"description":"Transition an order through the server matrix."},"CreateFulfillmentRequest":{"type":"object","properties":{"provider":{"type":["string","null"],"maxLength":100},"tracking_number":{"type":["string","null"],"maxLength":200},"status":{"default":"shipped","description":"\"pending\" reserves the quantities without auto-shipping the order (carrier booked, pickup not yet made); move to \"shipped\" via PATCH when the parcel leaves.","type":"string","enum":["pending","shipped"]},"lines":{"minItems":1,"maxItems":100,"type":"array","items":{"type":"object","properties":{"order_item_id":{"type":"string","format":"uuid"},"quantity":{"type":"integer","exclusiveMinimum":0}},"required":["order_item_id","quantity"],"additionalProperties":false}}},"required":["lines"],"additionalProperties":false,"description":"Fulfill selected line quantities. Each order item may appear once."},"UpdateFulfillmentRequest":{"type":"object","properties":{"tracking_number":{"type":["string","null"],"maxLength":200},"status":{"type":"string","enum":["shipped","delivered","cancelled"]},"cod_collected_minor":{"description":"COD remittance PROPOSAL: the cash the courier reports collecting (minor units; may be less than the order total). Moves no money. The merchant confirms in admin, and that confirmation writes the cod_collection ledger row which updates payment status and fires order.paid. One open proposal per order; re-sending updates it.","type":"integer","exclusiveMinimum":0},"cod_collected_note":{"type":"string","maxLength":300}},"additionalProperties":false,"minProperties":1,"description":"Update tracking, mark delivered, cancel, or propose a COD remittance. At least one field is required."},"CreateRefundRequest":{"type":"object","properties":{"amount_minor":{"type":"integer","exclusiveMinimum":0},"currency":{"description":"Optional confirmation guard; 409 when it differs from the order's currency.","type":"string","minLength":3,"maxLength":3},"reason":{"type":["string","null"],"maxLength":500},"lines":{"description":"Optional line breakdown; amounts must sum to amount_minor.","minItems":1,"maxItems":100,"type":"array","items":{"type":"object","properties":{"order_item_id":{"type":"string","format":"uuid"},"quantity":{"type":"integer","exclusiveMinimum":0},"amount_minor":{"type":"integer","exclusiveMinimum":0}},"required":["order_item_id","quantity","amount_minor"],"additionalProperties":false}},"restock":{"description":"Return the refunded quantities to stock. Requires `lines` and the inventory:write scope. Untracked products are skipped.","type":"object","properties":{"location_id":{"type":"string","format":"uuid"}},"required":["location_id"],"additionalProperties":false}},"required":["amount_minor"],"additionalProperties":false,"description":"Append a refund against captured funds."},"CreateInventoryMovementRequest":{"type":"object","properties":{"variant_id":{"type":"string","format":"uuid"},"location_id":{"type":"string","format":"uuid"},"delta":{"type":"integer","minimum":-1000000,"maximum":1000000,"not":{"const":0},"description":"Signed quantity change. Absolute stock writes are forbidden."},"reason":{"type":"string","enum":["correction","initial","return","damage"]},"note":{"type":["string","null"],"maxLength":500}},"required":["variant_id","location_id","delta","reason"],"description":"Append an inventory movement."},"UpsertWebhookSubscriptionRequest":{"type":"object","properties":{"endpoint_url":{"type":"string","maxLength":2000,"format":"uri","description":"Public HTTPS URL. Private, loopback and IP-literal hosts are refused."},"topics":{"minItems":1,"maxItems":12,"type":"array","items":{"$ref":"#/components/schemas/WebhookTopic"}},"rotate_secret":{"description":"Mint a new signing secret and return it once in the response.","type":"boolean"}},"required":["endpoint_url","topics"],"additionalProperties":false,"description":"Create or replace the install's single subscription."},"CreateWebhookTestEventRequest":{"type":"object","properties":{"topic":{"$ref":"#/components/schemas/WebhookTopic"},"data":{"description":"Optional overrides merged over the platform-built sample payload for the topic (at most 16 KB).","type":"object","additionalProperties":true}},"required":["topic"],"additionalProperties":false,"description":"Fire one signed test delivery (sandbox stores only)."},"OrderCreatedEventData":{"type":"object","properties":{"order_id":{"type":"string","format":"uuid"},"order_number":{"type":"string"},"status":{"type":"string","description":"Order status at creation (usually `placed`)."},"total_minor":{"type":"integer","description":"Integer minor units of `currency` (scale by the store's decimals)."},"currency":{"type":"string","minLength":3,"maxLength":3},"source":{"type":["string","null"],"description":"Sales channel (`storefront`, `pos`, `admin`, `app`). Emitted since revision 2026-09-10."}},"required":["order_id","order_number","status","total_minor","currency"],"additionalProperties":true},"OrderStatusChangedEventData":{"type":"object","properties":{"order_id":{"type":"string","format":"uuid"},"from_status":{"type":"string"},"status":{"type":"string","description":"The status the order moved to."}},"required":["order_id","from_status","status"],"additionalProperties":true},"OrderPaidEventData":{"type":"object","properties":{"order_id":{"type":"string","format":"uuid"},"amount_minor":{"type":"integer","description":"Captured funds at the moment they crossed the order total."},"currency":{"type":"string","minLength":3,"maxLength":3}},"required":["order_id","amount_minor","currency"],"additionalProperties":true,"description":"Fires when captured funds reach the order total, from the payments ledger only. COD orders reach it when the merchant confirms the courier's remittance, never on delivery."},"ProductCreatedEventData":{"type":"object","properties":{"product_id":{"type":"string","format":"uuid"}},"required":["product_id"],"additionalProperties":true},"ProductUpdatedEventData":{"type":"object","properties":{"product_id":{"type":"string","format":"uuid"},"variant_id":{"description":"Set when a variant row changed rather than the product itself.","type":"string","format":"uuid"},"operation":{"description":"Present on variant changes: which write on the variant fired the event.","type":"string","enum":["insert","update","delete"]}},"required":["product_id"],"additionalProperties":true},"ProductDeletedEventData":{"type":"object","properties":{"product_id":{"type":"string","format":"uuid"}},"required":["product_id"],"additionalProperties":true},"InventoryMovementCreatedEventData":{"type":"object","properties":{"movement_id":{"type":"string","format":"uuid"},"variant_id":{"type":["string","null"],"format":"uuid"},"location_id":{"type":"string","format":"uuid"},"delta":{"type":"integer"},"stock_after":{"type":"integer"},"reason":{"type":"string"}},"required":["movement_id","variant_id","location_id","delta","stock_after","reason"],"additionalProperties":true},"FulfillmentRequestedEventData":{"type":"object","properties":{"fulfillment_id":{"type":"string","format":"uuid"},"order_id":{"type":"string","format":"uuid"},"lines":{"type":"array","items":{"type":"object","properties":{"order_item_id":{"type":"string","format":"uuid"},"quantity":{"type":"integer","exclusiveMinimum":0}},"required":["order_item_id","quantity"],"additionalProperties":true}}},"required":["fulfillment_id","order_id","lines"],"additionalProperties":true,"description":"A fulfillment was created as `pending`: a pickup is waiting to be booked by a carrier app."},"FulfillmentCreatedEventData":{"type":"object","properties":{"fulfillment_id":{"type":"string","format":"uuid"},"order_id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["pending","shipped"]},"tracking_number":{"type":["string","null"]},"lines":{"type":"array","items":{"type":"object","properties":{"order_item_id":{"type":"string","format":"uuid"},"quantity":{"type":"integer","exclusiveMinimum":0}},"required":["order_item_id","quantity"],"additionalProperties":true}},"order_fully_fulfilled":{"type":"boolean","description":"True when every line of the order is now covered by a fulfillment."}},"required":["fulfillment_id","order_id","status","tracking_number","lines","order_fully_fulfilled"],"additionalProperties":true},"FulfillmentUpdatedEventData":{"type":"object","properties":{"fulfillment_id":{"type":"string","format":"uuid"},"order_id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["pending","shipped","delivered","cancelled"]},"previous_status":{"type":"string"},"tracking_number":{"type":["string","null"]}},"required":["fulfillment_id","order_id","status","previous_status","tracking_number"],"additionalProperties":true},"RefundCreatedEventData":{"type":"object","properties":{"refund_id":{"type":"string","format":"uuid"},"order_id":{"type":"string","format":"uuid"},"amount_minor":{"type":"integer","description":"Integer minor units of `currency` (scale by the store's decimals)."},"currency":{"type":"string","minLength":3,"maxLength":3},"lines":{"description":"Line breakdown when the refund carried one. Absent on POS refunds.","type":"array","items":{"type":"object","properties":{"order_item_id":{"type":["string","null"],"format":"uuid"},"quantity":{"type":"integer","exclusiveMinimum":0},"amount_minor":{"type":"integer","description":"Integer minor units of `currency` (scale by the store's decimals)."}},"required":["order_item_id","quantity","amount_minor"],"additionalProperties":true}},"source":{"description":"Present when the refund was written at the point of sale.","type":"string","const":"pos"}},"required":["refund_id","order_id","amount_minor","currency"],"additionalProperties":true},"AppUninstalledEventData":{"type":"object","properties":{"app_id":{"type":"string","format":"uuid"},"install_id":{"type":"string","format":"uuid"}},"required":["app_id","install_id"],"additionalProperties":true,"description":"Farewell notice: the merchant removed the app. Tokens are already dead; delete what the DPA requires and stop calling the API for this install."}}},"x-dukkan-api-version":"2026-09-10","x-dukkan-topic-scopes":{"order.created":"orders:read","order.status_changed":"orders:read","order.paid":"orders:read","product.created":"products:read","product.updated":"products:read","product.deleted":"products:read","inventory.movement_created":"inventory:read","fulfillment.requested":"orders:read","fulfillment.created":"orders:read","fulfillment.updated":"orders:read","refund.created":"orders:read","app.uninstalled":null},"x-dukkan-topic-payloads":{"order.created":{"$ref":"#/components/schemas/OrderCreatedEventData"},"order.status_changed":{"$ref":"#/components/schemas/OrderStatusChangedEventData"},"order.paid":{"$ref":"#/components/schemas/OrderPaidEventData"},"product.created":{"$ref":"#/components/schemas/ProductCreatedEventData"},"product.updated":{"$ref":"#/components/schemas/ProductUpdatedEventData"},"product.deleted":{"$ref":"#/components/schemas/ProductDeletedEventData"},"inventory.movement_created":{"$ref":"#/components/schemas/InventoryMovementCreatedEventData"},"fulfillment.requested":{"$ref":"#/components/schemas/FulfillmentRequestedEventData"},"fulfillment.created":{"$ref":"#/components/schemas/FulfillmentCreatedEventData"},"fulfillment.updated":{"$ref":"#/components/schemas/FulfillmentUpdatedEventData"},"refund.created":{"$ref":"#/components/schemas/RefundCreatedEventData"},"app.uninstalled":{"$ref":"#/components/schemas/AppUninstalledEventData"}},"x-dukkan-order-status-transitions":{"placed":["approved","processing","shipped","delivered","cancelled"],"approved":["processing","shipped","delivered","cancelled"],"processing":["shipped","delivered","cancelled"],"shipped":["delivered","delivered_failed"],"delivered":[],"delivered_failed":["shipped","returned"],"returned":[],"cancelled":[],"completed":["cancelled"],"partially_refunded":["cancelled"],"refunded":[]}}}