تخطَّ إلى المحتوى

@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.

الحزمة
@dukkan.one/app-sdk@0.3.0
مراجعة الواجهة
2026-09-10
الاستيراد
import { … } from "@dukkan.one/app-sdk"
في هذه الصفحة

الدوال

base64UrlToBytes#

function base64UrlToBytes(text: string): Uint8Array

Decodes base64url with or without padding.

buildAuthorizeUrl#

function buildAuthorizeUrl(input: AuthorizeUrlInput): string

The consent URL a merchant is sent to (docs/apps/authorization#authorize).

bytesToBase64Url#

function bytesToBase64Url(bytes: Uint8Array): string

base64url without padding, as PKCE and state values are encoded.

bytesToHex#

function bytesToHex(bytes: Uint8Array): string

Lowercase hex encoding.

canonicalJson#

function canonicalJson(value: unknown): string

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#

function canTransitionOrder(from: OrderStatus, to: SettableOrderStatus): boolean

Whether an app may PATCH an order from from to to (the server stays authoritative).

collectAll#

function collectAll<T>(fetchPage: PageFetcher<T>, options?: { maxItems?: number; }): Promise<T[]>

Collects every item; use only when the total is known to be small.

createAesGcmSealer#

function createAesGcmSealer(options: { currentKey: string; previousKey?: string | null; }): SecretSealer

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#

function createDukkanClient(options: DukkanClientOptions): DukkanClient

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#

function createKeyValueTokenStore(options: KeyValueTokenStoreOptions): TokenStore

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#

function deterministicIdempotencyKey(namespace: string, ...parts: Array<string | number>): Promise<string>

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#

function errorFromResponse(response: Response, context?: { method?: string; path?: string; parsedBody?: unknown; }): Promise<DukkanApiError>

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#

function exchangeCode(input: ExchangeCodeInput): Promise<TokenResponse>

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#

function generatePkce(): Promise<PkcePair>

PKCE S256 (RFC 7636); pinned by packages/contracts/vectors/pkce.json.

generateState#

function generateState(): string

A random state value (16 bytes, base64url) to bind the callback to the request that started it.

hexToBytes#

function hexToBytes(hex: string): Uint8Array | null

Decodes hex; returns null for odd length or non-hex input instead of throwing.

hmacSha256Hex#

function hmacSha256Hex(secret: string | Uint8Array, message: string | Uint8Array): Promise<string>

HMAC-SHA256 as lowercase hex: the webhook signature primitive.

hmacSha256Verify#

function hmacSha256Verify(secret: string | Uint8Array, message: string | Uint8Array, signature: Uint8Array): Promise<boolean>

Constant-time HMAC check: WebCrypto's verify compares inside the runtime, so the comparison never leaks through timing in JavaScript land.

iterateItems#

function iterateItems<T>(fetchPage: PageFetcher<T>, options?: { maxPages?: number; }): AsyncGenerator<T, void, undefined>

Walks a cursor-paginated list one item at a time, fetching the next page only when the current one is exhausted.

iteratePages#

function iteratePages<T>(fetchPage: PageFetcher<T>, options?: { maxPages?: number; }): AsyncGenerator<Page<T>, void, undefined>

Walks cursor pages. Two guards stop a misbehaving server from producing an endless loop: a cursor that repeats, and a hard page ceiling.

parseCallbackQuery#

function parseCallbackQuery(query: URLSearchParams | Record<string, string | undefined>): { ok: true; code: string; state: string; } | { ok: false; error: string; state: string; }

Parses the query the platform appends to redirect_uri. A consent denial or a platform-side refusal arrives as error (with state echoed).

pkceChallenge#

function pkceChallenge(verifier: string): Promise<string>

The S256 challenge for a verifier: base64url(sha256(verifier)).

randomBytes#

function randomBytes(length: number): Uint8Array

Cryptographically random bytes from WebCrypto.

randomIdempotencyKey#

function randomIdempotencyKey(): string

Random key for a write that has no natural identity (reused across the SDK's own retries).

randomUuid#

function randomUuid(): string

A random UUID v4.

redactForLog#

function redactForLog<T>(value: T): T

Deep-redacts a value for logging: strings are scrubbed, keys that name a secret are replaced wholesale, and Pii-marked fields are dropped.

redactSecrets#

function redactSecrets(text: string): string

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#

function refreshTokens(input: RefreshInput): Promise<TokenResponse>

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#

function requestHash(payload: unknown): Promise<string>

sha256 of the canonical JSON of a payload: the value the platform compares when the same key arrives with a different body.

sha256Hex#

function sha256Hex(input: string | Uint8Array): Promise<string>

SHA-256 as lowercase hex.

timingSafeEqual#

function timingSafeEqual(a: Uint8Array, b: Uint8Array): boolean

Constant-time equality for equal-length byte strings (lengths differ → false, fast).

tokensFromResponse#

function tokensFromResponse(installId: string, previous: Pick<StoredTokens, "storeId" | "scopes"> | null, response: TokenResponse, now?: () => number): StoredTokens

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#

function unwrapPii<T extends string | null>(value: Pii<T>): T

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.

الأصناف

DukkanApiError#

class DukkanApiError extends DukkanError

new DukkanApiError(init

A non-2xx answer from the Platform API, already parsed into the documented envelope.

الأعضاء
الاسمالنوعالوصف
statusمطلوبnumber
requestIdمطلوبstring | undefined
detailsمطلوبRecord<string, unknown> | undefined
retryAfterSecondsمطلوبnumber | undefined
methodمطلوبstring | undefined
pathمطلوبstring | undefined
retryableمطلوبboolean

True for statuses a client may retry unchanged (the SDK already did, up to its budget).

codeمطلوبstring
nameمطلوبstring
messageمطلوبstring
stackstring | undefined
causeunknown

DukkanAuthError#

class DukkanAuthError extends DukkanApiError

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.

الأعضاء
الاسمالنوعالوصف
reasonمطلوبAuthFailureReason
statusمطلوبnumber
requestIdمطلوبstring | undefined
detailsمطلوبRecord<string, unknown> | undefined
retryAfterSecondsمطلوبnumber | undefined
methodمطلوبstring | undefined
pathمطلوبstring | undefined
retryableمطلوبboolean

True for statuses a client may retry unchanged (the SDK already did, up to its budget).

codeمطلوبstring
nameمطلوبstring
messageمطلوبstring
stackstring | undefined
causeunknown

DukkanClient#

class 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.

الأعضاء
الاسمالنوعالوصف
installIdمطلوبstring
tokensمطلوبTokenManager
rawمطلوبClient<paths, `${string}/${string}`>

The openapi-fetch client for calls the facades do not cover; same transport and errors.

installationمطلوب{ get: () => Promise<Installation>; }

Identity of this install and its store (no scope needed).

ordersمطلوب{ 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>>; }
productsمطلوب{ 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[]>; }
storeمطلوب{ get: () => Promise<Store>; }

Public identity and checkout facts of the store (no scope needed): currencies, the FX table checkout applies, payment methods, provinces.

discountsمطلوب{ 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>; }
inventoryمطلوب{ 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>>; }; }
webhooksمطلوب{ 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#

class DukkanConfigError extends DukkanError

new DukkanConfigError(message

The app's own configuration (options, dukkan.app.toml) is unusable.

الأعضاء
الاسمالنوعالوصف
codeمطلوبstring
nameمطلوبstring
messageمطلوبstring
stackstring | undefined
causeunknown

DukkanConflictError#

class DukkanConflictError extends DukkanApiError

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).

الأعضاء
الاسمالنوعالوصف
detailsمطلوبConflictDetails | undefined
insufficientLinesمطلوبArray<InsufficientStockLine>

The insufficient_stock lines, or an empty list for any other conflict.

statusمطلوبnumber
requestIdمطلوبstring | undefined
retryAfterSecondsمطلوبnumber | undefined
methodمطلوبstring | undefined
pathمطلوبstring | undefined
retryableمطلوبboolean

True for statuses a client may retry unchanged (the SDK already did, up to its budget).

codeمطلوبstring
nameمطلوبstring
messageمطلوبstring
stackstring | undefined
causeunknown

DukkanError#

class DukkanError extends Error

new DukkanError(code

Base of every error the SDK throws; code is stable for programmatic handling.

الأعضاء
الاسمالنوعالوصف
codeمطلوبstring
nameمطلوبstring
messageمطلوبstring
stackstring | undefined
causeunknown

DukkanNetworkError#

class DukkanNetworkError extends DukkanError

new DukkanNetworkError(message

The network or the runtime failed before a response existed (timeouts included).

الأعضاء
الاسمالنوعالوصف
methodمطلوبstring | undefined
pathمطلوبstring | undefined
codeمطلوبstring
nameمطلوبstring
messageمطلوبstring
stackstring | undefined
causeunknown

DukkanOAuthError#

class DukkanOAuthError extends DukkanError

new DukkanOAuthError(error

An OAuth error body ({ error }) from the authorize or token endpoint.

الأعضاء
الاسمالنوعالوصف
statusمطلوبnumber
errorمطلوبstring
codeمطلوبstring
nameمطلوبstring
messageمطلوبstring
stackstring | undefined
causeunknown

DukkanRateLimitError#

class DukkanRateLimitError extends DukkanApiError

new DukkanRateLimitError(init

429 with the server's Retry-After already parsed.

الأعضاء
الاسمالنوعالوصف
statusمطلوبnumber
requestIdمطلوبstring | undefined
detailsمطلوبRecord<string, unknown> | undefined
retryAfterSecondsمطلوبnumber | undefined
methodمطلوبstring | undefined
pathمطلوبstring | undefined
retryableمطلوبboolean

True for statuses a client may retry unchanged (the SDK already did, up to its budget).

codeمطلوبstring
nameمطلوبstring
messageمطلوبstring
stackstring | undefined
causeunknown

DukkanReconnectRequiredError#

class DukkanReconnectRequiredError extends DukkanError

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.

الأعضاء
الاسمالنوعالوصف
installIdمطلوبstring
reasonمطلوبstring
codeمطلوبstring
nameمطلوبstring
messageمطلوبstring
stackstring | undefined
causeunknown

DukkanValidationError#

class DukkanValidationError extends DukkanApiError

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.

الأعضاء
الاسمالنوعالوصف
detailsمطلوبValidationDetails
fieldPathsمطلوبArray<string>

The rejected paths, in the platform's order.

statusمطلوبnumber
requestIdمطلوبstring | undefined
retryAfterSecondsمطلوبnumber | undefined
methodمطلوبstring | undefined
pathمطلوبstring | undefined
retryableمطلوبboolean

True for statuses a client may retry unchanged (the SDK already did, up to its budget).

codeمطلوبstring
nameمطلوبstring
messageمطلوبstring
stackstring | undefined
causeunknown

DukkanWebhookError#

class DukkanWebhookError extends DukkanError

new DukkanWebhookError(reason

A delivery the receiver refused; reason maps to the small JSON error body it answered.

الأعضاء
الاسمالنوعالوصف
reasonمطلوبWebhookRejection
statusمطلوبnumber
codeمطلوبstring
nameمطلوبstring
messageمطلوبstring
stackstring | undefined
causeunknown

IdempotencyConflictError#

class IdempotencyConflictError extends DukkanConflictError

new IdempotencyConflictError(init

409: the same Idempotency-Key was used with a different payload, or is still in progress.

الأعضاء
الاسمالنوعالوصف
inProgressمطلوبboolean
detailsمطلوبConflictDetails | undefined
insufficientLinesمطلوبArray<InsufficientStockLine>

The insufficient_stock lines, or an empty list for any other conflict.

statusمطلوبnumber
requestIdمطلوبstring | undefined
retryAfterSecondsمطلوبnumber | undefined
methodمطلوبstring | undefined
pathمطلوبstring | undefined
retryableمطلوبboolean

True for statuses a client may retry unchanged (the SDK already did, up to its budget).

codeمطلوبstring
nameمطلوبstring
messageمطلوبstring
stackstring | undefined
causeunknown

InvalidTransitionError#

class InvalidTransitionError extends DukkanConflictError

new InvalidTransitionError(init

409: the order's status matrix has no edge for the requested transition.

الأعضاء
الاسمالنوعالوصف
currentStatusمطلوبstring | undefined
detailsمطلوبConflictDetails | undefined
insufficientLinesمطلوبArray<InsufficientStockLine>

The insufficient_stock lines, or an empty list for any other conflict.

statusمطلوبnumber
requestIdمطلوبstring | undefined
retryAfterSecondsمطلوبnumber | undefined
methodمطلوبstring | undefined
pathمطلوبstring | undefined
retryableمطلوبboolean

True for statuses a client may retry unchanged (the SDK already did, up to its budget).

codeمطلوبstring
nameمطلوبstring
messageمطلوبstring
stackstring | undefined
causeunknown

MemoryTokenStore#

class MemoryTokenStore implements TokenStore

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.

الأعضاء
الاسمالنوعالوصف
loadمطلوب(installId: string): Promise<StoredTokens | null>
saveمطلوب(tokens: StoredTokens): Promise<void>
withRefreshLockمطلوب<T>(installId: string, fn: (locked: TokenStore) => Promise<T>): Promise<T>

Run fn while holding an exclusive per-install lock (row lock, lease, mutex). fn receives a store bound to that lock (for Postgres, the transaction that holds the row); every load and save inside the callback must go through it, never through the outer store.

markReconnectRequiredمطلوب(installId: string, reason: string): Promise<void>

Record that the install must go through consent again; subsequent loads carry the reason.

snapshotمطلوب(installId: string): StoredTokens | null

ScopeLostError#

class ScopeLostError extends DukkanApiError

new ScopeLostError(init

403: the install no longer holds a scope this call (or topic) needs.

الأعضاء
الاسمالنوعالوصف
topicsمطلوبArray<string>

Topics named by the server when a subscription request was refused.

statusمطلوبnumber
requestIdمطلوبstring | undefined
detailsمطلوبRecord<string, unknown> | undefined
retryAfterSecondsمطلوبnumber | undefined
methodمطلوبstring | undefined
pathمطلوبstring | undefined
retryableمطلوبboolean

True for statuses a client may retry unchanged (the SDK already did, up to its budget).

codeمطلوبstring
nameمطلوبstring
messageمطلوبstring
stackstring | undefined
causeunknown

TokenManager#

class 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.

الأعضاء
الاسمالنوعالوصف
installIdمطلوبstring
getAccessTokenمطلوب(options?: { forceRefresh?: boolean; }): Promise<string>

A token that is valid for at least the leeway; refreshes when needed.

getTokensمطلوب(): Promise<StoredTokens>

The stored row, refreshed when stale.

الواجهات

components#

interface components
الأعضاء
الاسمالنوعالوصف
schemasمطلوب{ 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; }; }
responsesمطلوب{ 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"]; }; }; }
parametersمطلوب{ Id: string; Cursor: string; Limit: number; IdempotencyKey: string; }
requestBodiesمطلوبnever
headersمطلوبnever
pathItemsمطلوبnever

ConflictDetails#

interface ConflictDetails extends Record<string, unknown>

details of a 409 as the SDK types it: lines is present for insufficient_stock, every other key passes through.

الأعضاء
الاسمالنوعالوصف
linesArray<InsufficientStockLine> | undefined
current_statusstring | undefined

DukkanClientOptions#

interface DukkanClientOptions

Configuration of createDukkanClient: the install, the token store and credentials, the platform origin, and optional fetch, timeout, retry and logging hooks.

الأعضاء
الاسمالنوعالوصف
installIdمطلوبstring

The install this client acts for (from the token response or GET /installation).

tokenStoreمطلوبTokenStore
credentialsمطلوبOAuthClientCredentials
apiUrlstring | undefined

Platform origin (default https://dukkan.one).

fetch((input: RequestInfo | URL, init?: RequestInit) => Promise<Response>) | undefined
timeoutMsnumber | undefined
retryPartial<RetryPolicy> | undefined
apiVersionstring | undefined
onApiVersionDrift((seen: string, pinned: string) => void) | undefined
loggerDukkanLogger | undefined
userAgentstring | undefined

DukkanLogger#

interface DukkanLogger

A minimal logger the SDK writes to; every line passes through redaction.

الأعضاء
الاسمالنوعالوصف
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#

interface IdempotencyOptions

How a write chooses its Idempotency-Key: an explicit key, or parts that identify the write in your own system.

الأعضاء
الاسمالنوعالوصف
keystring | undefined

Explicit key; wins over ref.

refArray<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#

interface InsufficientStockLine

One short line of an insufficient_stock conflict: what was asked for and what the store can sell.

الأعضاء
الاسمالنوعالوصف
variant_idمطلوبstring
requestednumber | 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.

availablenumber | undefined

KeyValueBackend#

interface 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.

الأعضاء
الاسمالنوعالوصف
getمطلوب(key: string): Promise<string | null>
putمطلوب(key: string, value: string, options?: { ttlSeconds?: number; }): Promise<void>
deleteمطلوب(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#

interface OAuthClientCredentials

Your app's client id and secret. The secret may be a function so a rotated value is read at call time.

الأعضاء
الاسمالنوعالوصف
clientIdمطلوبstring
clientSecretمطلوبstring | (() => string)

The secret, or a function returning it (read at call time so rotation needs no restart).

operations#

interface operations
الأعضاء
الاسمالنوعالوصف
getInstallationمطلوب{ 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"]; }; }
getStoreمطلوب{ 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"]; }; }
listOrdersمطلوب{ 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"]; }; }
createOrderمطلوب{ 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"]; }; }
getOrderمطلوب{ 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"]; }; }
updateOrderمطلوب{ 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"]; }; }
updateOrderStatusمطلوب{ 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"]; }; }
createFulfillmentمطلوب{ 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"]; }; }
updateFulfillmentمطلوب{ 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"]; }; }
createRefundمطلوب{ 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"]; }; }
getWebhookSubscriptionمطلوب{ 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"]; }; }
upsertWebhookSubscriptionمطلوب{ 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"]; }; }
deleteWebhookSubscriptionمطلوب{ 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"]; }; }
createWebhookTestEventمطلوب{ 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"]; }; }
listProductsمطلوب{ 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"]; }; }
getProductمطلوب{ 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"]; }; }
listVariantsمطلوب{ 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"]; }; }
listDiscountsمطلوب{ 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"]; }; }
listInventoryMovementsمطلوب{ 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"]; }; }
createInventoryMovementمطلوب{ 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"]; }; }
receiveStoreEventمطلوب{ 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#

interface Page<T>

A cursor page as every list endpoint returns it.

الأعضاء
الاسمالنوعالوصف
dataمطلوبArray<T>
next_cursorمطلوبstring | null

paths#

interface paths
الأعضاء
الاسمالنوعالوصف
/installationمطلوب{ 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; }
/storeمطلوب{ 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; }
/ordersمطلوب{ 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}مطلوب{ 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}/statusمطلوب{ 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}/fulfillmentsمطلوب{ 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}مطلوب{ 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}/refundsمطلوب{ 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; }
/webhooksمطلوب{ 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/testمطلوب{ 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; }
/productsمطلوب{ 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}مطلوب{ 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/variantsمطلوب{ 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; }
/discountsمطلوب{ 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/movementsمطلوب{ 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#

interface PkcePair

A PKCE verifier and its S256 challenge.

الأعضاء
الاسمالنوعالوصف
verifierمطلوبstring

43 base64url characters; keep it server-side until the callback.

challengeمطلوبstring

S256 challenge to send on the authorize URL.

PlatformErrorBody#

interface PlatformErrorBody

The JSON body every Platform API error carries: a machine code, a message, the request id and optional details.

الأعضاء
الاسمالنوعالوصف
errorمطلوب{ code: PlatformErrorCode; message: string; request_id?: string; details?: Record<string, unknown>; }

RetryPolicy#

interface 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-After is 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.
الأعضاء
الاسمالنوعالوصف
attemptsمطلوبnumber

Total attempts including the first (default 3).

baseDelayMsمطلوبnumber

Base delay for backoff in milliseconds (default 500).

maxDelayMsمطلوبnumber

Ceiling for one backoff wait in milliseconds (default 8000).

maxRetryAfterSecondsمطلوبnumber

Longest Retry-After the SDK will sleep for; longer values throw (default 30).

SecretSealer#

interface SecretSealer

Envelope encryption for tokens at rest (AES-256-GCM helper shipped in ./sealer).

الأعضاء
الاسمالنوعالوصف
sealمطلوب(plaintext: string): Promise<string>
openمطلوب(envelope: string): Promise<string>

StoredTokens#

interface 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.

الأعضاء
الاسمالنوعالوصف
installIdمطلوبstring
storeIdمطلوبstring
accessTokenمطلوبstring
accessExpiresAtمطلوبstring

RFC 3339 instant after which the access token is refused.

refreshTokenمطلوبstring
refreshExpiresAtstring | null | undefined
scopesArray<string> | undefined
reconnectRequiredstring | null | undefined

Set when the platform said the install needs re-consent; calls throw until re-authorized.

TokenResponse#

interface TokenResponse

What /apps/oauth/token returns on both grants (identity fields since 2026-09-09).

الأعضاء
الاسمالنوعالوصف
access_tokenمطلوبstring
token_typeمطلوب"Bearer"
expires_inمطلوبnumber
refresh_tokenمطلوبstring
scopestring | undefined
install_idstring | undefined
store_idstring | undefined
store_slugstring | undefined

TokenStore#

interface 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.

الأعضاء
الاسمالنوعالوصف
loadمطلوب(installId: string): Promise<StoredTokens | null>
saveمطلوب(tokens: StoredTokens): Promise<void>
withRefreshLockمطلوب<T>(installId: string, fn: (locked: TokenStore) => Promise<T>): Promise<T>

Run fn while holding an exclusive per-install lock (row lock, lease, mutex). fn receives a store bound to that lock (for Postgres, the transaction that holds the row); every load and save inside the callback must go through it, never through the outer store.

markReconnectRequiredمطلوب(installId: string, reason: string): Promise<void>

Record that the install must go through consent again; subsequent loads carry the reason.

ValidationDetails#

interface ValidationDetails extends Record<string, unknown>

details of a 422: fields names every rejected path.

الأعضاء
الاسمالنوعالوصف
fieldsمطلوبArray<ValidationFieldIssue>

ValidationFieldIssue#

interface ValidationFieldIssue

One field the platform rejected in a 422, with the JSON path and the reason.

الأعضاء
الاسمالنوعالوصف
pathمطلوبstring
messageمطلوبstring

WriteOptions#

interface WriteOptions

Options every write accepts: how to choose its Idempotency-Key.

الأعضاء
الاسمالنوعالوصف
idempotencyIdempotencyOptions | undefined

WriteResult#

interface WriteResult<T>

What a write returns: the data, whether the platform replayed a recorded response, the key that was sent, and the request id.

الأعضاء
الاسمالنوعالوصف
dataمطلوبT
replayedمطلوبboolean

True when the platform replayed a recorded response for this idempotency key.

idempotencyKeyمطلوبstring
requestIdمطلوبstring | null

الأنواع

AppScope#

type AppScope = (typeof APP_SCOPES)[number]

AuthFailureReason#

type AuthFailureReason = "access_removed" | "no_permissions" | "invalid_token"

CreateOrderItem#

type CreateOrderItem = Schemas["CreateOrderItem"]

CreateOrderRequest#

type CreateOrderRequest = Schemas["CreateOrderRequest"]

Discount#

type Discount = Schemas["Discount"]

Installation#

type Installation = Schemas["Installation"]

InventoryMovement#

type InventoryMovement = Schemas["Movement"]

InventoryMovementReason#

type InventoryMovementReason = (typeof INVENTORY_MOVEMENT_REASONS)[number]

ListProductsQuery#

type ListProductsQuery = Omit<RawListProductsQuery, "ids"> & { ids?: readonly string[] }

GET /products filters; ids is a list here and joined with commas on the wire (at most 100).

Money#

type Money = Schemas["Money"]

Order#

type Order = Schemas["Order"]

OrderConflictCode#

type OrderConflictCode = "insufficient_stock" | "variant_unavailable" | "payment_method_disabled"

Machine codes a 409 from POST /orders carries besides the generic conflict.

OrderDetail#

type OrderDetail = Schemas["OrderDetail"]

OrderItem#

type OrderItem = Schemas["OrderItem"]

OrderStatus#

type OrderStatus = (typeof ORDER_STATUSES)[number]

PageFetcher#

type PageFetcher<T> = (cursor: string | undefined) => Promise<Page<T>>

PaymentStatus#

type PaymentStatus = (typeof PAYMENT_STATUSES)[number]

Pii#

type Pii<T extends string | null = string> = T & { readonly __pii: unique symbol }

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#

type PlatformErrorCode = | "invalid_request" | "unauthorized" | "forbidden" | "not_found" | "conflict" | "payload_too_large" | "rate_limited" | "internal_error" | "insufficient_stock" | "variant_unavailable" | "payment_method_disabled" | "variant_not_found" | "currency_not_allowed" | (string & {})

Machine codes the Platform API puts in error.code (docs/reference/api#errors).

Product#

type Product = Schemas["Product"]

Schemas#

type Schemas = components["schemas"]

SearchProductsQuery#

type SearchProductsQuery = Omit<ListProductsQuery, "q"> & { q: string }

products.search: q is required, the other filters are optional.

SettableOrderStatus#

type SettableOrderStatus = (typeof SETTABLE_ORDER_STATUSES)[number]

Store#

type Store = Schemas["Store"]

UpdateOrderRequest#

type UpdateOrderRequest = Schemas["UpdateOrderRequest"]

Variant#

type Variant = Schemas["Variant"]

VariantDetail#

type VariantDetail = Schemas["VariantDetail"]

WebhookRejection#

type WebhookRejection = | "missing_headers" | "stale_timestamp" | "bad_signature" | "body_too_large" | "malformed_envelope" | "unknown_install" | "store_mismatch"

WebhookSubscription#

type WebhookSubscription = Schemas["WebhookSubscription"]

WebhookTestEvent#

type WebhookTestEvent = Schemas["WebhookTestEvent"]

WebhookTopic#

type WebhookTopic = (typeof WEBHOOK_TOPICS)[number]

الثوابت

APP_PII_SCOPES#

const APP_PII_SCOPES: readonly ["clients:read", "clients:write"]

Scopes that unlock customer contact PII; the portal lets only DPA-accepted developers request them.

APP_SCOPES#

const APP_SCOPES: readonly ["clients:read", "clients:write", "discounts:read", "fulfillments:write", "inventory:read", "inventory:write", "orders:create", "orders:read", "orders:write", "products:read", "refunds:write"]

Every scope an app can request, in the platform's canonical order.

DEFAULT_API_URL#

const DEFAULT_API_URL: "https://dukkan.one"

Where the merchant platform lives; every OAuth and API path hangs off it.

DEFAULT_RETRY_POLICY#

const DEFAULT_RETRY_POLICY: RetryPolicy

IDEMPOTENCY_KEY_MAX_LENGTH#

const IDEMPOTENCY_KEY_MAX_LENGTH: 200

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#

const IDENTITY_SEALER: SecretSealer

A sealer that stores plaintext. Only for tests and throwaway tools; production stores use createAesGcmSealer or their own.

INVENTORY_MOVEMENT_REASONS#

const INVENTORY_MOVEMENT_REASONS: readonly ["correction", "initial", "return", "damage"]

Accepted reason values of POST /inventory/movements.

MAX_IDS_PER_LOOKUP#

const MAX_IDS_PER_LOOKUP: 100

GET /products?ids= and GET /products/variants?ids= accept at most this many ids per call.

ORDER_STATUS_TRANSITIONS#

const ORDER_STATUS_TRANSITIONS: { readonly placed: readonly ["approved", "processing", "shipped", "delivered", "cancelled"]; readonly approved: readonly ["processing", "shipped", "delivered", "cancelled"]; readonly processing: readonly ["shipped", "delivered", "cancelled"]; readonly shipped: readonly ["delivered", "delivered_failed"]; readonly delivered: readonly []; readonly delivered_failed: readonly ["shipped", "returned"]; readonly returned: readonly []; readonly cancelled: readonly []; readonly completed: readonly ["cancelled"]; readonly partially_refunded: readonly ["cancelled"]; readonly refunded: readonly []; }

Transitions an app may request through PATCH /orders/{id}/status, per current status.

ORDER_STATUSES#

const ORDER_STATUSES: readonly ["placed", "approved", "processing", "shipped", "delivered", "delivered_failed", "returned", "cancelled", "completed", "partially_refunded", "refunded"]

Every order status the platform reports.

PAYMENT_STATUSES#

const PAYMENT_STATUSES: readonly ["not_required", "unpaid", "pending", "authorized", "partially_paid", "paid", "partially_refunded", "refunded", "failed"]

Every payment_status value: a projection of the payments ledger, never set directly.

PLATFORM_API_VERSION#

const PLATFORM_API_VERSION: "2026-09-10"

Dated v1 revision this SDK was generated against (X-Dukkan-Api-Version).

RETRYABLE_STATUSES#

const RETRYABLE_STATUSES: Set<number>

SDK_USER_AGENT#

const SDK_USER_AGENT: string

Default User-Agent of every Platform API call: dukkan-app-sdk/<version>.

SDK_VERSION#

const SDK_VERSION: string

The package version, injected from package.json at build time (tsup define; vitest does the same).

SETTABLE_ORDER_STATUSES#

const SETTABLE_ORDER_STATUSES: readonly ["placed", "approved", "processing", "shipped", "delivered", "delivered_failed", "returned", "cancelled"]

The statuses an app may set through PATCH /orders/{id}/status.

TOPIC_REQUIRED_SCOPE#

const TOPIC_REQUIRED_SCOPE: { readonly "order.created": "orders:read"; readonly "order.status_changed": "orders:read"; readonly "order.paid": "orders:read"; readonly "product.created": "products:read"; readonly "product.updated": "products:read"; readonly "product.deleted": "products:read"; readonly "inventory.movement_created": "inventory:read"; readonly "fulfillment.requested": "orders:read"; readonly "fulfillment.created": "orders:read"; readonly "fulfillment.updated": "orders:read"; readonly "refund.created": "orders:read"; readonly "app.uninstalled": null; }

Read scope an install must hold to receive a topic; null for lifecycle topics.

WEBHOOK_TOPICS#

const WEBHOOK_TOPICS: readonly ["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"]

Every topic an app can subscribe to, from x-dukkan-topic-scopes.