@dukkan.one/app-sdk
The client, the token manager, errors, retry and pagination helpers, plus every symbol of the other entry points re-exported for one-line imports.
- Package
@dukkan.one/app-sdk@0.3.0- API revision
2026-09-10- Import
import { … } from "@dukkan.one/app-sdk"
On this page
- Functions
- base64UrlToBytes
- buildAuthorizeUrl
- bytesToBase64Url
- bytesToHex
- canonicalJson
- canTransitionOrder
- collectAll
- createAesGcmSealer
- createDukkanClient
- createKeyValueTokenStore
- deterministicIdempotencyKey
- errorFromResponse
- exchangeCode
- generatePkce
- generateState
- hexToBytes
- hmacSha256Hex
- hmacSha256Verify
- iterateItems
- iteratePages
- parseCallbackQuery
- pkceChallenge
- randomBytes
- randomIdempotencyKey
- randomUuid
- redactForLog
- redactSecrets
- refreshTokens
- requestHash
- sha256Hex
- timingSafeEqual
- tokensFromResponse
- unwrapPii
- Classes
- DukkanApiError
- DukkanAuthError
- DukkanClient
- DukkanConfigError
- DukkanConflictError
- DukkanError
- DukkanNetworkError
- DukkanOAuthError
- DukkanRateLimitError
- DukkanReconnectRequiredError
- DukkanValidationError
- DukkanWebhookError
- IdempotencyConflictError
- InvalidTransitionError
- MemoryTokenStore
- ScopeLostError
- TokenManager
- Interfaces
- components
- ConflictDetails
- DukkanClientOptions
- DukkanLogger
- IdempotencyOptions
- InsufficientStockLine
- KeyValueBackend
- OAuthClientCredentials
- operations
- Page
- paths
- PkcePair
- PlatformErrorBody
- RetryPolicy
- SecretSealer
- StoredTokens
- TokenResponse
- TokenStore
- ValidationDetails
- ValidationFieldIssue
- WriteOptions
- WriteResult
- Types
- AppScope
- AuthFailureReason
- CreateOrderItem
- CreateOrderRequest
- Discount
- Installation
- InventoryMovement
- InventoryMovementReason
- ListProductsQuery
- Money
- Order
- OrderConflictCode
- OrderDetail
- OrderItem
- OrderStatus
- PageFetcher
- PaymentStatus
- Pii
- PlatformErrorCode
- Product
- Schemas
- SearchProductsQuery
- SettableOrderStatus
- Store
- UpdateOrderRequest
- Variant
- VariantDetail
- WebhookRejection
- WebhookSubscription
- WebhookTestEvent
- WebhookTopic
- Constants
- APP_PII_SCOPES
- APP_SCOPES
- DEFAULT_API_URL
- DEFAULT_RETRY_POLICY
- IDEMPOTENCY_KEY_MAX_LENGTH
- IDENTITY_SEALER
- INVENTORY_MOVEMENT_REASONS
- MAX_IDS_PER_LOOKUP
- ORDER_STATUS_TRANSITIONS
- ORDER_STATUSES
- PAYMENT_STATUSES
- PLATFORM_API_VERSION
- RETRYABLE_STATUSES
- SDK_USER_AGENT
- SDK_VERSION
- SETTABLE_ORDER_STATUSES
- TOPIC_REQUIRED_SCOPE
- WEBHOOK_TOPICS
Functions
base64UrlToBytes#
Decodes base64url with or without padding.
buildAuthorizeUrl#
The consent URL a merchant is sent to (docs/apps/authorization#authorize).
bytesToBase64Url#
base64url without padding, as PKCE and state values are encoded.
bytesToHex#
Lowercase hex encoding.
canonicalJson#
Canonical JSON exactly as the platform hashes request bodies (packages/contracts/vectors/canonical-json.json): keys sorted at every depth, arrays in order, no whitespace. Useful to detect, client-side, that two payloads would collide on one key.
canTransitionOrder#
Whether an app may PATCH an order from from to to (the server stays authoritative).
collectAll#
Collects every item; use only when the total is known to be small.
createAesGcmSealer#
AES-256-GCM sealer with zero-downtime key rotation: seal with the current
key, open with current then previous. Keys are 32 bytes, base64. The wire
format (iv.tag.ciphertext, each base64) matches the first-party apps'
lib/crypto.mjs, so migrating an existing token table needs no rewrite.
createDukkanClient#
Creates a typed Platform API client for one install. Reads are retried on 429 and 5xx; writes carry an Idempotency-Key; a rejected access token is refreshed once through the token store's lock.
createKeyValueTokenStore#
Token store over a key-value backend. Token rows are sealed before they
reach the backend; the refresh lock is a short lease taken with
putIfAbsent and polled by waiters.
deterministicIdempotencyKey#
Deterministic key from your own entity identity, so a crashed process that
re-runs the same job sends the same key and gets the recorded result
instead of a second refund: deterministicIdempotencyKey("refund", orderId, jobId).
Namespaced and hashed, so the parts can be any length and never leak.
errorFromResponse#
Turns a failed response into the most specific error the SDK knows. The body is the documented envelope when the platform produced it; anything else (a proxy page, an empty body) still yields a usable error.
exchangeCode#
Redeems the authorization code from the callback for the first token pair. The response carries install_id, store_id and store_slug; key your storage on the ids.
generatePkce#
PKCE S256 (RFC 7636); pinned by packages/contracts/vectors/pkce.json.
generateState#
A random state value (16 bytes, base64url) to bind the callback to the request that started it.
hexToBytes#
Decodes hex; returns null for odd length or non-hex input instead of throwing.
hmacSha256Hex#
HMAC-SHA256 as lowercase hex: the webhook signature primitive.
hmacSha256Verify#
Constant-time HMAC check: WebCrypto's verify compares inside the runtime, so the comparison never leaks through timing in JavaScript land.
iterateItems#
Walks a cursor-paginated list one item at a time, fetching the next page only when the current one is exhausted.
iteratePages#
Walks cursor pages. Two guards stop a misbehaving server from producing an endless loop: a cursor that repeats, and a hard page ceiling.
parseCallbackQuery#
Parses the query the platform appends to redirect_uri. A consent denial
or a platform-side refusal arrives as error (with state echoed).
pkceChallenge#
The S256 challenge for a verifier: base64url(sha256(verifier)).
randomBytes#
Cryptographically random bytes from WebCrypto.
randomIdempotencyKey#
Random key for a write that has no natural identity (reused across the SDK's own retries).
randomUuid#
A random UUID v4.
redactForLog#
Deep-redacts a value for logging: strings are scrubbed, keys that name a
secret are replaced wholesale, and Pii-marked fields are dropped.
redactSecrets#
Replaces every platform token or secret in a string (dk_app_at_…, dk_whsec_…, …) with a redacted marker; every SDK error message passes through it.
refreshTokens#
Presents a refresh token for a new pair. Rotation is single-use: presenting the same token twice revokes the install (a 30 second leeway covers a race between two instances). Prefer TokenManager, which serialises refreshes through your store's lock.
requestHash#
sha256 of the canonical JSON of a payload: the value the platform compares when the same key arrives with a different body.
sha256Hex#
SHA-256 as lowercase hex.
timingSafeEqual#
Constant-time equality for equal-length byte strings (lengths differ → false, fast).
tokensFromResponse#
Builds the StoredTokens row from a token response, computing accessExpiresAt from expires_in and keeping the previous store id and scopes when the response omits them.
unwrapPii#
Reads a Pii-branded value where a plain string is needed (a message body, a rendered template). Keep the brand everywhere else so PII never reaches a log by accident.
Classes
DukkanApiError#
new DukkanApiError(init
A non-2xx answer from the Platform API, already parsed into the documented envelope.
| Name | Type | Description |
|---|---|---|
statusRequired | number | |
requestIdRequired | string | undefined | |
detailsRequired | Record<string, unknown> | undefined | |
retryAfterSecondsRequired | number | undefined | |
methodRequired | string | undefined | |
pathRequired | string | undefined | |
retryableRequired | boolean | True for statuses a client may retry unchanged (the SDK already did, up to its budget). |
codeRequired | string | |
nameRequired | string | |
messageRequired | string | |
stack | string | undefined | |
cause | unknown |
DukkanAuthError#
new DukkanAuthError(init
401 from the API. invalid_token means refresh and retry (the SDK does
that once); the other two mean the merchant changed something and the app
must send them back through consent.
| Name | Type | Description |
|---|---|---|
reasonRequired | AuthFailureReason | |
statusRequired | number | |
requestIdRequired | string | undefined | |
detailsRequired | Record<string, unknown> | undefined | |
retryAfterSecondsRequired | number | undefined | |
methodRequired | string | undefined | |
pathRequired | string | undefined | |
retryableRequired | boolean | True for statuses a client may retry unchanged (the SDK already did, up to its budget). |
codeRequired | string | |
nameRequired | string | |
messageRequired | string | |
stack | string | undefined | |
cause | unknown |
DukkanClient#
new DukkanClient(options
Typed Platform API client for one install. Every method throws a typed
DukkanError on failure; reads paginate through async iterators; writes
attach an Idempotency-Key (random, or derived from your own reference)
and report whether the platform replayed a recorded result.
| Name | Type | Description |
|---|---|---|
installIdRequired | string | |
tokensRequired | TokenManager | |
rawRequired | Client<paths, `${string}/${string}`> | The openapi-fetch client for calls the facades do not cover; same transport and errors. |
installationRequired | { get: () => Promise<Installation>; } | Identity of this install and its store (no scope needed). |
ordersRequired | { list: (query?: ListOrdersQuery) => Promise<Page<Order>>; pages: (query?: Omit<ListOrdersQuery, "cursor">) => AsyncGenerator<Page<{ id: string; order_number: string; status: components["schemas"]["OrderStatus"]; payment_status: components["schemas"]["PaymentStatus"]; source: string | null; line_pricing: "net" | "gross"; subtotal: components["schemas"]["Money"]; discount: components["schemas"]["Money"]; shipping: components["schemas"]["Money"]; tax: components["schemas"]["Money"]; total: components["schemas"]["Money"]; paid: components["schemas"]["Money"]; refunded: components["schemas"]["Money"]; exchange_rate: { scaled: number; scale: number; } | null; tags: string[]; created_at: string; updated_at: string; }>, void, undefined>; iterate: (query?: Omit<ListOrdersQuery, "cursor">) => AsyncGenerator<{ id: string; order_number: string; status: components["schemas"]["OrderStatus"]; payment_status: components["schemas"]["PaymentStatus"]; source: string | null; line_pricing: "net" | "gross"; subtotal: components["schemas"]["Money"]; discount: components["schemas"]["Money"]; shipping: components["schemas"]["Money"]; tax: components["schemas"]["Money"]; total: components["schemas"]["Money"]; paid: components["schemas"]["Money"]; refunded: components["schemas"]["Money"]; exchange_rate: { scaled: number; scale: number; } | null; tags: string[]; created_at: string; updated_at: string; }, void, undefined>; get: (id: string) => Promise<OrderDetail>; create: (body: CreateOrderRequest, options?: WriteOptions) => Promise<WriteResult<OrderDetail>>; update: (id: string, body: UpdateOrderRequest, options?: WriteOptions) => Promise<WriteResult<OrderDetail>>; updateStatus: (id: string, status: SettableOrderStatus, options?: WriteOptions & { currentStatus?: OrderStatus; }) => Promise<WriteResult<unknown>>; createFulfillment: (id: string, body: Schemas["CreateFulfillmentRequest"], options?: WriteOptions) => Promise<WriteResult<unknown>>; updateFulfillment: (id: string, fulfillmentId: string, body: Schemas["UpdateFulfillmentRequest"], options?: WriteOptions) => Promise<WriteResult<unknown>>; createRefund: (id: string, body: Schemas["CreateRefundRequest"], options?: WriteOptions) => Promise<WriteResult<unknown>>; } | |
productsRequired | { list: (query?: ListProductsQuery) => Promise<Page<Product>>; search: (query: SearchProductsQuery) => Promise<Page<Product>>; pages: (query?: Omit<ListProductsQuery, "cursor">) => AsyncGenerator<Page<{ id: string; dsin: string; name: string; status: string; updated_at: string | null; image_url: string | null; image_color: string | null; brand: string | null; category: null | components["schemas"]["ProductCategory"]; description: string | null; images?: string[]; variants: components["schemas"]["Variant"][]; }>, void, undefined>; iterate: (query?: Omit<ListProductsQuery, "cursor">) => AsyncGenerator<{ id: string; dsin: string; name: string; status: string; updated_at: string | null; image_url: string | null; image_color: string | null; brand: string | null; category: null | components["schemas"]["ProductCategory"]; description: string | null; images?: string[]; variants: components["schemas"]["Variant"][]; }, void, undefined>; get: (id: string) => Promise<Product>; variants: (ids: readonly string[]) => Promise<VariantDetail[]>; } | |
storeRequired | { get: () => Promise<Store>; } | Public identity and checkout facts of the store (no scope needed): currencies, the FX table checkout applies, payment methods, provinces. |
discountsRequired | { list: (query?: ListDiscountsQuery) => Promise<Page<Discount>>; pages: (query?: Omit<ListDiscountsQuery, "cursor">) => AsyncGenerator<Page<{ id: string; method: "code" | "automatic"; code: string | null; title: string | null; value_type: "percentage" | "fixed_amount" | "free_shipping" | "buy_x_get_y"; target: "order" | "products" | "categories"; percent: number | null; amount: null | components["schemas"]["Money"]; is_active: boolean; usage_limit_total: number | null; used_count: number; applies_to_pos: boolean; applies_to_online: boolean; starts_at: string | null; ends_at: string | null; created_at: string; updated_at: string | null; }>, void, undefined>; iterate: (query?: Omit<ListDiscountsQuery, "cursor">) => AsyncGenerator<{ id: string; method: "code" | "automatic"; code: string | null; title: string | null; value_type: "percentage" | "fixed_amount" | "free_shipping" | "buy_x_get_y"; target: "order" | "products" | "categories"; percent: number | null; amount: null | components["schemas"]["Money"]; is_active: boolean; usage_limit_total: number | null; used_count: number; applies_to_pos: boolean; applies_to_online: boolean; starts_at: string | null; ends_at: string | null; created_at: string; updated_at: string | null; }, void, undefined>; } | |
inventoryRequired | { movements: { list: (query?: ListMovementsQuery) => Promise<Page<InventoryMovement>>; iterate: (query?: Omit<ListMovementsQuery, "cursor">) => AsyncGenerator<{ id: string; sequence: number; variant_id: string | null; location_id: string; delta: number; stock_after: number; reason: string; occurred_at: string; }, void, undefined>; create: (body: Schemas["CreateInventoryMovementRequest"], options?: WriteOptions) => Promise<WriteResult<unknown>>; }; } | |
webhooksRequired | { get: () => Promise<WebhookSubscription | null>; upsert: (body: Schemas["UpsertWebhookSubscriptionRequest"]) => Promise<WebhookSubscription>; delete: () => Promise<void>; ensureSubscription: (input: { endpointUrl: string; topics: readonly WebhookTopic[]; hasSecret: boolean; rotateSecret?: boolean; }) => Promise<{ subscription: WebhookSubscription; secret: string | null; }>; test: (topic: WebhookTopic, data?: Record<string, unknown>) => Promise<WebhookTestEvent>; } |
DukkanConfigError#
new DukkanConfigError(message
The app's own configuration (options, dukkan.app.toml) is unusable.
| Name | Type | Description |
|---|---|---|
codeRequired | string | |
nameRequired | string | |
messageRequired | string | |
stack | string | undefined | |
cause | unknown |
DukkanConflictError#
new DukkanConflictError(init
409 of any kind; subclasses narrow the ones the SDK can name. code is the
specific platform code when there is one (insufficient_stock,
variant_unavailable, payment_method_disabled), else conflict, and
details.lines is typed for insufficient_stock (nothing was written).
| Name | Type | Description |
|---|---|---|
detailsRequired | ConflictDetails | undefined | |
insufficientLinesRequired | Array<InsufficientStockLine> | The |
statusRequired | number | |
requestIdRequired | string | undefined | |
retryAfterSecondsRequired | number | undefined | |
methodRequired | string | undefined | |
pathRequired | string | undefined | |
retryableRequired | boolean | True for statuses a client may retry unchanged (the SDK already did, up to its budget). |
codeRequired | string | |
nameRequired | string | |
messageRequired | string | |
stack | string | undefined | |
cause | unknown |
DukkanError#
new DukkanError(code
Base of every error the SDK throws; code is stable for programmatic handling.
| Name | Type | Description |
|---|---|---|
codeRequired | string | |
nameRequired | string | |
messageRequired | string | |
stack | string | undefined | |
cause | unknown |
DukkanNetworkError#
new DukkanNetworkError(message
The network or the runtime failed before a response existed (timeouts included).
| Name | Type | Description |
|---|---|---|
methodRequired | string | undefined | |
pathRequired | string | undefined | |
codeRequired | string | |
nameRequired | string | |
messageRequired | string | |
stack | string | undefined | |
cause | unknown |
DukkanOAuthError#
new DukkanOAuthError(error
An OAuth error body ({ error }) from the authorize or token endpoint.
| Name | Type | Description |
|---|---|---|
statusRequired | number | |
errorRequired | string | |
codeRequired | string | |
nameRequired | string | |
messageRequired | string | |
stack | string | undefined | |
cause | unknown |
DukkanRateLimitError#
new DukkanRateLimitError(init
429 with the server's Retry-After already parsed.
| Name | Type | Description |
|---|---|---|
statusRequired | number | |
requestIdRequired | string | undefined | |
detailsRequired | Record<string, unknown> | undefined | |
retryAfterSecondsRequired | number | undefined | |
methodRequired | string | undefined | |
pathRequired | string | undefined | |
retryableRequired | boolean | True for statuses a client may retry unchanged (the SDK already did, up to its budget). |
codeRequired | string | |
nameRequired | string | |
messageRequired | string | |
stack | string | undefined | |
cause | unknown |
DukkanReconnectRequiredError#
new DukkanReconnectRequiredError(installId
The install has no usable tokens: the refresh token was revoked, reuse was detected, or the store removed the app. Send the merchant through consent again; every API call for this install throws this until then.
| Name | Type | Description |
|---|---|---|
installIdRequired | string | |
reasonRequired | string | |
codeRequired | string | |
nameRequired | string | |
messageRequired | string | |
stack | string | undefined | |
cause | unknown |
DukkanValidationError#
new DukkanValidationError(init
422: the body was well-formed JSON but fails the contract (a missing
shipping_address for delivery, a discount above the lines, a currency the
store does not sell in: currency_not_allowed). details.fields lists the
paths; fix the payload, never retry unchanged.
| Name | Type | Description |
|---|---|---|
detailsRequired | ValidationDetails | |
fieldPathsRequired | Array<string> | The rejected paths, in the platform's order. |
statusRequired | number | |
requestIdRequired | string | undefined | |
retryAfterSecondsRequired | number | undefined | |
methodRequired | string | undefined | |
pathRequired | string | undefined | |
retryableRequired | boolean | True for statuses a client may retry unchanged (the SDK already did, up to its budget). |
codeRequired | string | |
nameRequired | string | |
messageRequired | string | |
stack | string | undefined | |
cause | unknown |
DukkanWebhookError#
new DukkanWebhookError(reason
A delivery the receiver refused; reason maps to the small JSON error body it answered.
| Name | Type | Description |
|---|---|---|
reasonRequired | WebhookRejection | |
statusRequired | number | |
codeRequired | string | |
nameRequired | string | |
messageRequired | string | |
stack | string | undefined | |
cause | unknown |
IdempotencyConflictError#
new IdempotencyConflictError(init
409: the same Idempotency-Key was used with a different payload, or is still in progress.
| Name | Type | Description |
|---|---|---|
inProgressRequired | boolean | |
detailsRequired | ConflictDetails | undefined | |
insufficientLinesRequired | Array<InsufficientStockLine> | The |
statusRequired | number | |
requestIdRequired | string | undefined | |
retryAfterSecondsRequired | number | undefined | |
methodRequired | string | undefined | |
pathRequired | string | undefined | |
retryableRequired | boolean | True for statuses a client may retry unchanged (the SDK already did, up to its budget). |
codeRequired | string | |
nameRequired | string | |
messageRequired | string | |
stack | string | undefined | |
cause | unknown |
InvalidTransitionError#
new InvalidTransitionError(init
409: the order's status matrix has no edge for the requested transition.
| Name | Type | Description |
|---|---|---|
currentStatusRequired | string | undefined | |
detailsRequired | ConflictDetails | undefined | |
insufficientLinesRequired | Array<InsufficientStockLine> | The |
statusRequired | number | |
requestIdRequired | string | undefined | |
retryAfterSecondsRequired | number | undefined | |
methodRequired | string | undefined | |
pathRequired | string | undefined | |
retryableRequired | boolean | True for statuses a client may retry unchanged (the SDK already did, up to its budget). |
codeRequired | string | |
nameRequired | string | |
messageRequired | string | |
stack | string | undefined | |
cause | unknown |
MemoryTokenStore#
new MemoryTokenStore(initial?
Single-process store for tests, CLIs and scripts. The lock is a per-install promise chain, which is exactly what one process needs and nothing a second process could rely on: never use this behind more than one instance.
| Name | Type | Description |
|---|---|---|
loadRequired | (installId: string): Promise<StoredTokens | null> | |
saveRequired | (tokens: StoredTokens): Promise<void> | |
withRefreshLockRequired | <T>(installId: string, fn: (locked: TokenStore) => Promise<T>): Promise<T> | Run |
markReconnectRequiredRequired | (installId: string, reason: string): Promise<void> | Record that the install must go through consent again; subsequent loads carry the reason. |
snapshotRequired | (installId: string): StoredTokens | null |
ScopeLostError#
new ScopeLostError(init
403: the install no longer holds a scope this call (or topic) needs.
| Name | Type | Description |
|---|---|---|
topicsRequired | Array<string> | Topics named by the server when a subscription request was refused. |
statusRequired | number | |
requestIdRequired | string | undefined | |
detailsRequired | Record<string, unknown> | undefined | |
retryAfterSecondsRequired | number | undefined | |
methodRequired | string | undefined | |
pathRequired | string | undefined | |
retryableRequired | boolean | True for statuses a client may retry unchanged (the SDK already did, up to its budget). |
codeRequired | string | |
nameRequired | string | |
messageRequired | string | |
stack | string | undefined | |
cause | unknown |
TokenManager#
new TokenManager(options
Hands out a valid access token for one install.
Refresh discipline (ADR 0012): one refresh at a time per install in this
process (single flight), and one at a time across processes through the
store's lock. Inside the lock the row is re-read: if a sibling already
rotated, its pair is used and no request is made. An invalid_grant gets
one more re-read (the platform answers it for a race it detected within
its 30 s leeway). If the row still holds the same refresh token, the lock
is released, the manager waits past the leeway, and presents the token
once more: a sibling that really rotated has saved by then, while a thief
who rotated our token is caught by the platform's reuse detection, which
revokes the whole family instead of leaving the stolen pair alive. Only
then is the install marked reconnect-required.
| Name | Type | Description |
|---|---|---|
installIdRequired | string | |
getAccessTokenRequired | (options?: { forceRefresh?: boolean; }): Promise<string> | A token that is valid for at least the leeway; refreshes when needed. |
getTokensRequired | (): Promise<StoredTokens> | The stored row, refreshed when stale. |
Interfaces
components#
| Name | Type | Description |
|---|---|---|
schemasRequired | { Error: { error: { code: string; message: string; request_id?: string; details?: { [key: string]: unknown; }; }; }; Money: { amount_minor: number; currency: string; decimals: number; }; OrderStatus: "placed" | "approved" | "processing" | "shipped" | "delivered" | "delivered_failed" | "returned" | "cancelled" | "completed" | "partially_refunded" | "refunded"; SettableOrderStatus: "placed" | "approved" | "processing" | "shipped" | "delivered" | "delivered_failed" | "returned" | "cancelled"; PaymentStatus: "not_required" | "unpaid" | "pending" | "authorized" | "partially_paid" | "paid" | "partially_refunded" | "refunded" | "failed"; WebhookTopic: "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"; WebhookEnvelope: { id: string; api_version: "v1"; topic: components["schemas"]["WebhookTopic"]; store_id: string; install_id: string; sequence: number; occurred_at: string; test?: boolean; data: { [key: string]: unknown; }; }; Order: { id: string; order_number: string; status: components["schemas"]["OrderStatus"]; payment_status: components["schemas"]["PaymentStatus"]; source: string | null; line_pricing: "net" | "gross"; subtotal: components["schemas"]["Money"]; discount: components["schemas"]["Money"]; shipping: components["schemas"]["Money"]; tax: components["schemas"]["Money"]; total: components["schemas"]["Money"]; paid: components["schemas"]["Money"]; refunded: components["schemas"]["Money"]; exchange_rate: { scaled: number; scale: number; } | null; tags: string[]; created_at: string; updated_at: string; }; OrderItem: { id: string; variant_id: string | null; kind: "variant" | "custom"; name: string; quantity: number; unit_price: components["schemas"]["Money"]; total: components["schemas"]["Money"]; }; OrderCustomer: { first_name: string | null; last_name: string | null; email: string | null; phone: string | null; }; FulfillmentLine: { order_item_id: string; quantity: number; }; Fulfillment: { id: string; provider: string | null; tracking_number: string | null; status: "pending" | "shipped" | "delivered" | "cancelled"; created_at: string; updated_at: string; lines: components["schemas"]["FulfillmentLine"][]; }; RefundLine: { order_item_id: string | null; quantity: number; amount_minor: number; }; Refund: { id: string; amount: components["schemas"]["Money"]; reason: string | null; created_at: string; lines: components["schemas"]["RefundLine"][]; }; OrderReference: { kind: string; number: string; url?: string | null; }; OrderAttributes: { [key: string]: (string | number | boolean | null) | components["schemas"]["OrderReference"]; }; OrderAttributesPatch: { [key: string]: (string | number | boolean | null) | components["schemas"]["OrderReference"]; }; OrderDetail: { id: string; order_number: string; status: components["schemas"]["OrderStatus"]; payment_status: components["schemas"]["PaymentStatus"]; source: string | null; line_pricing: "net" | "gross"; subtotal: components["schemas"]["Money"]; discount: components["schemas"]["Money"]; shipping: components["schemas"]["Money"]; tax: components["schemas"]["Money"]; total: components["schemas"]["Money"]; paid: components["schemas"]["Money"]; refunded: components["schemas"]["Money"]; exchange_rate: { scaled: number; scale: number; } | null; tags: string[]; created_at: string; updated_at: string; items: components["schemas"]["OrderItem"][]; fulfillments: components["schemas"]["Fulfillment"][]; refunds: components["schemas"]["Refund"][]; note: string | null; attributes: components["schemas"]["OrderAttributes"]; customer?: components["schemas"]["OrderCustomer"]; shipping_address?: { [key: string]: unknown; } | null; }; Variant: { id: string; product_id: string; sku: string | null; price: components["schemas"]["Money"]; status: string; name: string; attributes: { [key: string]: string; }; barcode: string | null; compare_at_price: null | components["schemas"]["Money"]; image_url: string | null; weight_gram: number | null; available_quantity: number | null; }; VariantDetail: { id: string; product_id: string; sku: string | null; price: components["schemas"]["Money"]; status: string; name: string; attributes: { [key: string]: string; }; barcode: string | null; compare_at_price: null | components["schemas"]["Money"]; image_url: string | null; weight_gram: number | null; available_quantity: number | null; product_name: string; product_status: string; }; VariantList: { data: components["schemas"]["VariantDetail"][]; }; ProductCategory: { id: string; name: string; }; Product: { id: string; dsin: string; name: string; status: string; updated_at: string | null; image_url: string | null; image_color: string | null; brand: string | null; category: null | components["schemas"]["ProductCategory"]; description: string | null; images?: string[]; variants: components["schemas"]["Variant"][]; }; Store: { id: string; slug: string; name: string; description: string | null; logo_url: string | null; banner_urls: string[]; storefront_url: string; whatsapp_number: string | null; phone: string | null; address: string | null; accent_color: string | null; locale: string; timezone: string; country: string | null; pricing_currency: { code: string; decimals: number; }; storefront_currencies: { allowed: string[]; default: string; }; fx: { base: string; as_of: string; rates: { [key: string]: { scaled: number; scale: number; }; }; } | null; payment_methods: { id: "cod" | "shamcash_manual"; shamcash_id?: string; }[]; provinces: { value: string; ar: string; en: string; }[]; }; Discount: { id: string; method: "code" | "automatic"; code: string | null; title: string | null; value_type: "percentage" | "fixed_amount" | "free_shipping" | "buy_x_get_y"; target: "order" | "products" | "categories"; percent: number | null; amount: null | components["schemas"]["Money"]; is_active: boolean; usage_limit_total: number | null; used_count: number; applies_to_pos: boolean; applies_to_online: boolean; starts_at: string | null; ends_at: string | null; created_at: string; updated_at: string | null; }; Movement: { id: string; sequence: number; variant_id: string | null; location_id: string; delta: number; stock_after: number; reason: string; occurred_at: string; }; WebhookSubscription: { id: string; endpoint_url: string; topics: components["schemas"]["WebhookTopic"][]; status: "active" | "disabled"; consecutive_failures: number; activated_at: string; disabled_at: string | null; secret?: string; }; Installation: { install: { id: string; app_id: string; status: "active" | "suspended"; granted_scopes: string[]; effective_scopes: string[]; distribution_channel: "development" | "private" | "public"; installed_at: string; api_version: string; webhook: { id: string; endpoint_url: string; topics: components["schemas"]["WebhookTopic"][]; status: "active" | "disabled"; consecutive_failures: number; activated_at: string; disabled_at: string | null; } | null; }; store: { id: string; slug: string; name: string; currency: string; decimals: number; locale: string; timezone: string; country: string | null; is_development: boolean; }; }; WebhookTestEvent: { event_id: string; topic: components["schemas"]["WebhookTopic"]; sequence: number; occurred_at: string; test: true; data: { [key: string]: unknown; }; deliveries: { id: string; status: "pending" | "delivering" | "retry_scheduled" | "succeeded" | "dead"; endpoint_url: string; }[]; }; OrderPage: { data: components["schemas"]["Order"][]; next_cursor: string | null; }; ProductPage: { data: components["schemas"]["Product"][]; next_cursor: string | null; }; DiscountPage: { data: components["schemas"]["Discount"][]; next_cursor: string | null; }; MovementPage: { data: components["schemas"]["Movement"][]; next_cursor: string | null; }; CreateOrderItem: { variant_id: string | null; name?: string; quantity: number; unit_price_minor: number; }; CreateOrderRequest: { currency: string; status?: "placed" | "approved"; items: components["schemas"]["CreateOrderItem"][]; discount?: { amount_minor: number; title?: string | null; } | null; shipping: { amount_minor: number; method: "delivery" | "pickup"; }; customer: { name: string; phone: string; email?: string | null; }; shipping_address?: { address: string; province: string; city?: string | null; notes?: string | null; } | null; payment_method: "cod" | "shamcash_manual"; exchange_rate?: { scaled: number; scale: number; } | null; note?: string | null; tags?: string[]; attributes?: components["schemas"]["OrderAttributes"]; inventory?: "decrement" | "skip"; }; UpdateOrderRequest: { note?: string | null; tags?: { add?: string[]; remove?: string[]; }; attributes?: components["schemas"]["OrderAttributesPatch"]; }; UpdateOrderStatusRequest: { status: components["schemas"]["SettableOrderStatus"]; }; CreateFulfillmentRequest: { provider?: string | null; tracking_number?: string | null; status?: "pending" | "shipped"; lines: { order_item_id: string; quantity: number; }[]; }; UpdateFulfillmentRequest: { tracking_number?: string | null; status?: "shipped" | "delivered" | "cancelled"; cod_collected_minor?: number; cod_collected_note?: string; }; CreateRefundRequest: { amount_minor: number; currency?: string; reason?: string | null; lines?: { order_item_id: string; quantity: number; amount_minor: number; }[]; restock?: { location_id: string; }; }; CreateInventoryMovementRequest: { variant_id: string; location_id: string; delta: number; reason: "correction" | "initial" | "return" | "damage"; note?: string | null; }; UpsertWebhookSubscriptionRequest: { endpoint_url: string; topics: components["schemas"]["WebhookTopic"][]; rotate_secret?: boolean; }; CreateWebhookTestEventRequest: { topic: components["schemas"]["WebhookTopic"]; data?: { [key: string]: unknown; }; }; OrderCreatedEventData: { order_id: string; order_number: string; status: string; total_minor: number; currency: string; source?: string | null; } & { [key: string]: unknown; }; OrderStatusChangedEventData: { order_id: string; from_status: string; status: string; } & { [key: string]: unknown; }; OrderPaidEventData: { order_id: string; amount_minor: number; currency: string; } & { [key: string]: unknown; }; ProductCreatedEventData: { product_id: string; } & { [key: string]: unknown; }; ProductUpdatedEventData: { product_id: string; variant_id?: string; operation?: "insert" | "update" | "delete"; } & { [key: string]: unknown; }; ProductDeletedEventData: { product_id: string; } & { [key: string]: unknown; }; InventoryMovementCreatedEventData: { movement_id: string; variant_id: string | null; location_id: string; delta: number; stock_after: number; reason: string; } & { [key: string]: unknown; }; FulfillmentRequestedEventData: { fulfillment_id: string; order_id: string; lines: ({ order_item_id: string; quantity: number; } & { [key: string]: unknown; })[]; } & { [key: string]: unknown; }; FulfillmentCreatedEventData: { fulfillment_id: string; order_id: string; status: "pending" | "shipped"; tracking_number: string | null; lines: ({ order_item_id: string; quantity: number; } & { [key: string]: unknown; })[]; order_fully_fulfilled: boolean; } & { [key: string]: unknown; }; FulfillmentUpdatedEventData: { fulfillment_id: string; order_id: string; status: "pending" | "shipped" | "delivered" | "cancelled"; previous_status: string; tracking_number: string | null; } & { [key: string]: unknown; }; RefundCreatedEventData: { refund_id: string; order_id: string; amount_minor: number; currency: string; lines?: ({ order_item_id: string | null; quantity: number; amount_minor: number; } & { [key: string]: unknown; })[]; source?: "pos"; } & { [key: string]: unknown; }; AppUninstalledEventData: { app_id: string; install_id: string; } & { [key: string]: unknown; }; } | |
responsesRequired | { BadRequest: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["Error"]; }; }; Unauthorized: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["Error"]; }; }; Forbidden: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["Error"]; }; }; NotFound: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["Error"]; }; }; UnprocessableEntity: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["Error"]; }; }; Conflict: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["Error"]; }; }; RateLimited: { headers: { "Retry-After"?: number; [name: string]: unknown; }; content: { "application/json": components["schemas"]["Error"]; }; }; } | |
parametersRequired | { Id: string; Cursor: string; Limit: number; IdempotencyKey: string; } | |
requestBodiesRequired | never | |
headersRequired | never | |
pathItemsRequired | never |
ConflictDetails#
details of a 409 as the SDK types it: lines is present for insufficient_stock, every other key passes through.
| Name | Type | Description |
|---|---|---|
lines | Array<InsufficientStockLine> | undefined | |
current_status | string | undefined |
DukkanClientOptions#
Configuration of createDukkanClient: the install, the token store and credentials, the platform origin, and optional fetch, timeout, retry and logging hooks.
| Name | Type | Description |
|---|---|---|
installIdRequired | string | The install this client acts for (from the token response or |
tokenStoreRequired | TokenStore | |
credentialsRequired | OAuthClientCredentials | |
apiUrl | string | undefined | Platform origin (default |
fetch | ((input: RequestInfo | URL, init?: RequestInit) => Promise<Response>) | undefined | |
timeoutMs | number | undefined | |
retry | Partial<RetryPolicy> | undefined | |
apiVersion | string | undefined | |
onApiVersionDrift | ((seen: string, pinned: string) => void) | undefined | |
logger | DukkanLogger | undefined | |
userAgent | string | undefined |
DukkanLogger#
A minimal logger the SDK writes to; every line passes through redaction.
| Name | Type | Description |
|---|---|---|
debug | ((message: string, data?: Record<string, unknown>) => void) | undefined | |
info | ((message: string, data?: Record<string, unknown>) => void) | undefined | |
warn | ((message: string, data?: Record<string, unknown>) => void) | undefined | |
error | ((message: string, data?: Record<string, unknown>) => void) | undefined |
IdempotencyOptions#
How a write chooses its Idempotency-Key: an explicit key, or parts that identify the write in your own system.
| Name | Type | Description |
|---|---|---|
key | string | undefined | Explicit key; wins over |
ref | Array<string | number> | undefined | Parts that identify the write in YOUR system (an order id plus a job id, a queue message id...). The SDK derives a stable key from them. |
InsufficientStockLine#
One short line of an insufficient_stock conflict: what was asked for and what the store can sell.
| Name | Type | Description |
|---|---|---|
variant_idRequired | string | |
requested | number | undefined | Present when the shortfall was detected up front. Absent when a concurrent order won the row lock: the server knows which variant ran out but not by how much, and reporting 0 there would be a lie. |
available | number | undefined |
KeyValueBackend#
The smallest surface a key-value backend needs (Cloudflare KV, Redis, Deno
KV, Upstash...). putIfAbsent is what makes the refresh lock real across
instances; without it the adapter falls back to an in-process mutex and
says so at construction.
| Name | Type | Description |
|---|---|---|
getRequired | (key: string): Promise<string | null> | |
putRequired | (key: string, value: string, options?: { ttlSeconds?: number; }): Promise<void> | |
deleteRequired | (key: string): Promise<void> | |
putIfAbsent | ((key: string, value: string, options: { ttlSeconds: number; }) => Promise<boolean>) | undefined | Atomic "set if not present"; returns false when the key already exists. |
OAuthClientCredentials#
Your app's client id and secret. The secret may be a function so a rotated value is read at call time.
| Name | Type | Description |
|---|---|---|
clientIdRequired | string | |
clientSecretRequired | string | (() => string) | The secret, or a function returning it (read at call time so rotation needs no restart). |
operations#
| Name | Type | Description |
|---|---|---|
getInstallationRequired | { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content: { "application/json": { data: components["schemas"]["Installation"]; }; }; }; 401: components["responses"]["Unauthorized"]; 429: components["responses"]["RateLimited"]; }; } | |
getStoreRequired | { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content: { "application/json": { data: components["schemas"]["Store"]; }; }; }; 401: components["responses"]["Unauthorized"]; 429: components["responses"]["RateLimited"]; }; } | |
listOrdersRequired | { parameters: { query?: { cursor?: components["parameters"]["Cursor"]; limit?: components["parameters"]["Limit"]; status?: components["schemas"]["OrderStatus"]; created_at_min?: string; updated_at_min?: string; tag?: string; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["OrderPage"]; }; }; 401: components["responses"]["Unauthorized"]; 403: components["responses"]["Forbidden"]; 429: components["responses"]["RateLimited"]; }; } | |
createOrderRequired | { parameters: { query?: never; header: { "Idempotency-Key": components["parameters"]["IdempotencyKey"]; }; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["CreateOrderRequest"]; }; }; responses: { 201: { headers: { [name: string]: unknown; }; content: { "application/json": { data: components["schemas"]["OrderDetail"]; }; }; }; 400: components["responses"]["BadRequest"]; 401: components["responses"]["Unauthorized"]; 403: components["responses"]["Forbidden"]; 404: components["responses"]["NotFound"]; 409: components["responses"]["Conflict"]; 422: components["responses"]["UnprocessableEntity"]; 429: components["responses"]["RateLimited"]; }; } | |
getOrderRequired | { parameters: { query?: never; header?: never; path: { id: components["parameters"]["Id"]; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content: { "application/json": { data: components["schemas"]["OrderDetail"]; }; }; }; 401: components["responses"]["Unauthorized"]; 403: components["responses"]["Forbidden"]; 404: components["responses"]["NotFound"]; 429: components["responses"]["RateLimited"]; }; } | |
updateOrderRequired | { parameters: { query?: never; header: { "Idempotency-Key": components["parameters"]["IdempotencyKey"]; }; path: { id: components["parameters"]["Id"]; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UpdateOrderRequest"]; }; }; responses: { 200: { headers: { [name: string]: unknown; }; content: { "application/json": { data: components["schemas"]["OrderDetail"]; }; }; }; 400: components["responses"]["BadRequest"]; 401: components["responses"]["Unauthorized"]; 403: components["responses"]["Forbidden"]; 404: components["responses"]["NotFound"]; 409: components["responses"]["Conflict"]; 422: components["responses"]["UnprocessableEntity"]; 429: components["responses"]["RateLimited"]; }; } | |
updateOrderStatusRequired | { parameters: { query?: never; header: { "Idempotency-Key": components["parameters"]["IdempotencyKey"]; }; path: { id: components["parameters"]["Id"]; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UpdateOrderStatusRequest"]; }; }; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; 400: components["responses"]["BadRequest"]; 401: components["responses"]["Unauthorized"]; 403: components["responses"]["Forbidden"]; 404: components["responses"]["NotFound"]; 409: components["responses"]["Conflict"]; 429: components["responses"]["RateLimited"]; }; } | |
createFulfillmentRequired | { parameters: { query?: never; header: { "Idempotency-Key": components["parameters"]["IdempotencyKey"]; }; path: { id: components["parameters"]["Id"]; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["CreateFulfillmentRequest"]; }; }; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; 400: components["responses"]["BadRequest"]; 401: components["responses"]["Unauthorized"]; 403: components["responses"]["Forbidden"]; 404: components["responses"]["NotFound"]; 409: components["responses"]["Conflict"]; 429: components["responses"]["RateLimited"]; }; } | |
updateFulfillmentRequired | { parameters: { query?: never; header: { "Idempotency-Key": components["parameters"]["IdempotencyKey"]; }; path: { id: components["parameters"]["Id"]; fulfillmentId: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UpdateFulfillmentRequest"]; }; }; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; 400: components["responses"]["BadRequest"]; 401: components["responses"]["Unauthorized"]; 403: components["responses"]["Forbidden"]; 404: components["responses"]["NotFound"]; 409: components["responses"]["Conflict"]; 429: components["responses"]["RateLimited"]; }; } | |
createRefundRequired | { parameters: { query?: never; header: { "Idempotency-Key": components["parameters"]["IdempotencyKey"]; }; path: { id: components["parameters"]["Id"]; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["CreateRefundRequest"]; }; }; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; 400: components["responses"]["BadRequest"]; 401: components["responses"]["Unauthorized"]; 403: components["responses"]["Forbidden"]; 404: components["responses"]["NotFound"]; 409: components["responses"]["Conflict"]; 429: components["responses"]["RateLimited"]; }; } | |
getWebhookSubscriptionRequired | { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content: { "application/json": { data: null | components["schemas"]["WebhookSubscription"]; }; }; }; 401: components["responses"]["Unauthorized"]; 429: components["responses"]["RateLimited"]; }; } | |
upsertWebhookSubscriptionRequired | { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UpsertWebhookSubscriptionRequest"]; }; }; responses: { 200: { headers: { [name: string]: unknown; }; content: { "application/json": { data: components["schemas"]["WebhookSubscription"]; }; }; }; 400: components["responses"]["BadRequest"]; 401: components["responses"]["Unauthorized"]; 403: components["responses"]["Forbidden"]; 404: components["responses"]["NotFound"]; 429: components["responses"]["RateLimited"]; }; } | |
deleteWebhookSubscriptionRequired | { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 204: { headers: { [name: string]: unknown; }; content?: never; }; 401: components["responses"]["Unauthorized"]; 429: components["responses"]["RateLimited"]; }; } | |
createWebhookTestEventRequired | { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["CreateWebhookTestEventRequest"]; }; }; responses: { 202: { headers: { [name: string]: unknown; }; content: { "application/json": { data: components["schemas"]["WebhookTestEvent"]; }; }; }; 400: components["responses"]["BadRequest"]; 401: components["responses"]["Unauthorized"]; 403: components["responses"]["Forbidden"]; 409: components["responses"]["Conflict"]; 413: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["Error"]; }; }; 429: components["responses"]["RateLimited"]; }; } | |
listProductsRequired | { parameters: { query?: { cursor?: components["parameters"]["Cursor"]; limit?: components["parameters"]["Limit"]; updated_at_min?: string; q?: string; status?: "active" | "out_of_stock" | "hidden" | "discontinued" | "no_variants"; ids?: string; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["ProductPage"]; }; }; 401: components["responses"]["Unauthorized"]; 403: components["responses"]["Forbidden"]; 429: components["responses"]["RateLimited"]; }; } | |
getProductRequired | { parameters: { query?: never; header?: never; path: { id: components["parameters"]["Id"]; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content: { "application/json": { data: components["schemas"]["Product"]; }; }; }; 401: components["responses"]["Unauthorized"]; 403: components["responses"]["Forbidden"]; 404: components["responses"]["NotFound"]; 429: components["responses"]["RateLimited"]; }; } | |
listVariantsRequired | { parameters: { query: { ids: string; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["VariantList"]; }; }; 400: components["responses"]["BadRequest"]; 401: components["responses"]["Unauthorized"]; 403: components["responses"]["Forbidden"]; 429: components["responses"]["RateLimited"]; }; } | |
listDiscountsRequired | { parameters: { query?: { cursor?: components["parameters"]["Cursor"]; limit?: components["parameters"]["Limit"]; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["DiscountPage"]; }; }; 401: components["responses"]["Unauthorized"]; 403: components["responses"]["Forbidden"]; 429: components["responses"]["RateLimited"]; }; } | |
listInventoryMovementsRequired | { parameters: { query?: { cursor?: components["parameters"]["Cursor"]; limit?: components["parameters"]["Limit"]; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["MovementPage"]; }; }; 401: components["responses"]["Unauthorized"]; 403: components["responses"]["Forbidden"]; 429: components["responses"]["RateLimited"]; }; } | |
createInventoryMovementRequired | { parameters: { query?: never; header: { "Idempotency-Key": components["parameters"]["IdempotencyKey"]; }; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["CreateInventoryMovementRequest"]; }; }; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; 400: components["responses"]["BadRequest"]; 401: components["responses"]["Unauthorized"]; 403: components["responses"]["Forbidden"]; 404: components["responses"]["NotFound"]; 409: components["responses"]["Conflict"]; 429: components["responses"]["RateLimited"]; }; } | |
receiveStoreEventRequired | { parameters: { query?: never; header: { "X-Dukkan-Delivery-Id": string; "X-Dukkan-Install-Id": string; "X-Dukkan-Event": components["schemas"]["WebhookTopic"]; "X-Dukkan-Timestamp": string; "X-Dukkan-Api-Version": string; "X-Dukkan-Hmac-Sha256": string; }; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["WebhookEnvelope"]; }; }; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; } |
Page#
A cursor page as every list endpoint returns it.
| Name | Type | Description |
|---|---|---|
dataRequired | Array<T> | |
next_cursorRequired | string | null |
paths#
| Name | Type | Description |
|---|---|---|
/installationRequired | { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get: operations["getInstallation"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; } | |
/storeRequired | { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get: operations["getStore"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; } | |
/ordersRequired | { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get: operations["listOrders"]; put?: never; post: operations["createOrder"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; } | |
/orders/{id}Required | { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get: operations["getOrder"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch: operations["updateOrder"]; trace?: never; } | |
/orders/{id}/statusRequired | { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; delete?: never; options?: never; head?: never; patch: operations["updateOrderStatus"]; trace?: never; } | |
/orders/{id}/fulfillmentsRequired | { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post: operations["createFulfillment"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; } | |
/orders/{id}/fulfillments/{fulfillmentId}Required | { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; delete?: never; options?: never; head?: never; patch: operations["updateFulfillment"]; trace?: never; } | |
/orders/{id}/refundsRequired | { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post: operations["createRefund"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; } | |
/webhooksRequired | { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get: operations["getWebhookSubscription"]; put: operations["upsertWebhookSubscription"]; post?: never; delete: operations["deleteWebhookSubscription"]; options?: never; head?: never; patch?: never; trace?: never; } | |
/webhooks/testRequired | { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post: operations["createWebhookTestEvent"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; } | |
/productsRequired | { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get: operations["listProducts"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; } | |
/products/{id}Required | { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get: operations["getProduct"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; } | |
/products/variantsRequired | { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get: operations["listVariants"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; } | |
/discountsRequired | { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get: operations["listDiscounts"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; } | |
/inventory/movementsRequired | { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get: operations["listInventoryMovements"]; put?: never; post: operations["createInventoryMovement"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; } |
PkcePair#
A PKCE verifier and its S256 challenge.
| Name | Type | Description |
|---|---|---|
verifierRequired | string | 43 base64url characters; keep it server-side until the callback. |
challengeRequired | string | S256 challenge to send on the authorize URL. |
PlatformErrorBody#
The JSON body every Platform API error carries: a machine code, a message, the request id and optional details.
| Name | Type | Description |
|---|---|---|
errorRequired | { code: PlatformErrorCode; message: string; request_id?: string; details?: Record<string, unknown>; } |
RetryPolicy#
Retry policy for Platform API calls (ADR 0012, App SDK plan):
- Reads (GET/HEAD/DELETE) always retry on 429 and transient 5xx.
- Writes retry ONLY when the request carries an
Idempotency-Key, which the client attaches to every write, so a retry can never double-apply. Retry-Afteris honoured up to a cap; beyond it the error surfaces.- Full-jitter exponential backoff otherwise, three attempts in total.
- 401 is never retried here; the transport refreshes once and re-issues.
| Name | Type | Description |
|---|---|---|
attemptsRequired | number | Total attempts including the first (default 3). |
baseDelayMsRequired | number | Base delay for backoff in milliseconds (default 500). |
maxDelayMsRequired | number | Ceiling for one backoff wait in milliseconds (default 8000). |
maxRetryAfterSecondsRequired | number | Longest |
SecretSealer#
Envelope encryption for tokens at rest (AES-256-GCM helper shipped in ./sealer).
| Name | Type | Description |
|---|---|---|
sealRequired | (plaintext: string): Promise<string> | |
openRequired | (envelope: string): Promise<string> |
StoredTokens#
What an app persists per install. Tokens are stored by the app (encrypted
at rest through a SecretSealer), never by the SDK: the SDK only reads,
refreshes and writes back through the TokenStore contract.
| Name | Type | Description |
|---|---|---|
installIdRequired | string | |
storeIdRequired | string | |
accessTokenRequired | string | |
accessExpiresAtRequired | string | RFC 3339 instant after which the access token is refused. |
refreshTokenRequired | string | |
refreshExpiresAt | string | null | undefined | |
scopes | Array<string> | undefined | |
reconnectRequired | string | null | undefined | Set when the platform said the install needs re-consent; calls throw until re-authorized. |
TokenResponse#
What /apps/oauth/token returns on both grants (identity fields since 2026-09-09).
| Name | Type | Description |
|---|---|---|
access_tokenRequired | string | |
token_typeRequired | "Bearer" | |
expires_inRequired | number | |
refresh_tokenRequired | string | |
scope | string | undefined | |
install_id | string | undefined | |
store_id | string | undefined | |
store_slug | string | undefined |
TokenStore#
Persistence contract every app implements once (adapters for Postgres and key-value stores ship with the SDK).
withRefreshLock is the important one: the platform revokes an install
when a rotated refresh token is presented again (theft detection), so two
instances must never refresh the same install concurrently. The lock
serialises refreshes across processes; the SDK re-reads inside it and
skips the refresh when a sibling already rotated.
| Name | Type | Description |
|---|---|---|
loadRequired | (installId: string): Promise<StoredTokens | null> | |
saveRequired | (tokens: StoredTokens): Promise<void> | |
withRefreshLockRequired | <T>(installId: string, fn: (locked: TokenStore) => Promise<T>): Promise<T> | Run |
markReconnectRequiredRequired | (installId: string, reason: string): Promise<void> | Record that the install must go through consent again; subsequent loads carry the reason. |
ValidationDetails#
details of a 422: fields names every rejected path.
| Name | Type | Description |
|---|---|---|
fieldsRequired | Array<ValidationFieldIssue> |
ValidationFieldIssue#
One field the platform rejected in a 422, with the JSON path and the reason.
| Name | Type | Description |
|---|---|---|
pathRequired | string | |
messageRequired | string |
WriteOptions#
Options every write accepts: how to choose its Idempotency-Key.
| Name | Type | Description |
|---|---|---|
idempotency | IdempotencyOptions | undefined |
WriteResult#
What a write returns: the data, whether the platform replayed a recorded response, the key that was sent, and the request id.
| Name | Type | Description |
|---|---|---|
dataRequired | T | |
replayedRequired | boolean | True when the platform replayed a recorded response for this idempotency key. |
idempotencyKeyRequired | string | |
requestIdRequired | string | null |
Types
AppScope#
AuthFailureReason#
CreateOrderItem#
CreateOrderRequest#
Discount#
Installation#
InventoryMovement#
InventoryMovementReason#
ListProductsQuery#
GET /products filters; ids is a list here and joined with commas on the wire (at most 100).
Money#
Order#
OrderConflictCode#
Machine codes a 409 from POST /orders carries besides the generic conflict.
OrderDetail#
OrderItem#
OrderStatus#
PageFetcher#
PaymentStatus#
Pii#
Brand for customer-contact fields (the DPA-gated clients:read scope).
The type is a plain string at runtime; the brand keeps PII from flowing
into places typed for ordinary strings (log lines, analytics, third-party
calls) without an explicit unwrapPii.
PlatformErrorCode#
Machine codes the Platform API puts in error.code (docs/reference/api#errors).
Product#
Schemas#
SearchProductsQuery#
products.search: q is required, the other filters are optional.
SettableOrderStatus#
Store#
UpdateOrderRequest#
Variant#
VariantDetail#
WebhookRejection#
WebhookSubscription#
WebhookTestEvent#
WebhookTopic#
Constants
APP_PII_SCOPES#
Scopes that unlock customer contact PII; the portal lets only DPA-accepted developers request them.
APP_SCOPES#
Every scope an app can request, in the platform's canonical order.
DEFAULT_API_URL#
Where the merchant platform lives; every OAuth and API path hangs off it.
DEFAULT_RETRY_POLICY#
IDEMPOTENCY_KEY_MAX_LENGTH#
Idempotency-Key helpers (docs/reference/api#idempotency). The platform
remembers a key for 24 hours per install: the same key with the same body
replays the recorded response, the same key with a different body is a 409.
IDENTITY_SEALER#
A sealer that stores plaintext. Only for tests and throwaway tools; production stores use createAesGcmSealer or their own.
INVENTORY_MOVEMENT_REASONS#
Accepted reason values of POST /inventory/movements.
MAX_IDS_PER_LOOKUP#
GET /products?ids= and GET /products/variants?ids= accept at most this many ids per call.
ORDER_STATUS_TRANSITIONS#
Transitions an app may request through PATCH /orders/{id}/status, per current status.
ORDER_STATUSES#
Every order status the platform reports.
PAYMENT_STATUSES#
Every payment_status value: a projection of the payments ledger, never set directly.
PLATFORM_API_VERSION#
Dated v1 revision this SDK was generated against (X-Dukkan-Api-Version).
RETRYABLE_STATUSES#
SDK_USER_AGENT#
Default User-Agent of every Platform API call: dukkan-app-sdk/<version>.
SDK_VERSION#
The package version, injected from package.json at build time (tsup define; vitest does the same).
SETTABLE_ORDER_STATUSES#
The statuses an app may set through PATCH /orders/{id}/status.
TOPIC_REQUIRED_SCOPE#
Read scope an install must hold to receive a topic; null for lifecycle topics.
WEBHOOK_TOPICS#
Every topic an app can subscribe to, from x-dukkan-topic-scopes.