Build an app in 15 minutes
From registering the app to the first signed webhook on a sandbox store.
On this page
This page is for anyone building an app that integrates with Dukkan stores. By the end you have a registered app, an access token for a sandbox store, a successful Platform API call, and a webhook subscription that received its first event.
1. Register and create your app#
- Sign in to the developer portal and enroll your organization. This requires the organization owner and a verified email.
- From the dashboard create an app: a name, at least one exact redirect URI (HTTPS only;
localhostis allowed for development), only the scopes the app actually needs, and the webhook topics it will subscribe to. - Copy the client secret the moment it appears; it is never shown again. To rotate it later see App lifecycle.
Note
Request the minimum scopes. The merchant sees every scope as a plain Arabic sentence on the consent screen, and effective scopes are always intersected with the granting member's live permissions. The full table is in the scope reference.
2. Create a sandbox store#
From "Sandbox stores" create a sandbox. It arrives with seeded data: products with variants, orders in every lifecycle state, ledger-backed cash-on-delivery payments, every discount type, and a USD order with an exchange-rate snapshot. Details and limits are in Sandbox stores.
3. Get an access token#
Dukkan uses OAuth 2.1 with PKCE, and your app is a confidential client: it sends the client secret and the code_verifier. Send the merchant to the consent URL:
GET https://dukkan.one/apps/oauth/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://app.example.com/callback
&scope=orders:read%20orders:write
&state=RANDOM_STATE
&code_challenge=S256_CHALLENGE
&code_challenge_method=S256After consent, a code arrives on your redirect URI together with the same state. Exchange it for tokens:
curl -X POST https://dukkan.one/apps/oauth/token \
-u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
-d grant_type=authorization_code \
-d code=AUTH_CODE \
-d redirect_uri=https://app.example.com/callback \
-d code_verifier=PKCE_VERIFIERconst basic = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString("base64");
const res = await fetch("https://dukkan.one/apps/oauth/token", {
method: "POST",
headers: {
Authorization: `Basic ${basic}`,
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
grant_type: "authorization_code",
code: AUTH_CODE,
redirect_uri: "https://app.example.com/callback",
code_verifier: PKCE_VERIFIER,
}),
});
const tokens = await res.json();import requests
res = requests.post(
"https://dukkan.one/apps/oauth/token",
auth=(CLIENT_ID, CLIENT_SECRET),
data={
"grant_type": "authorization_code",
"code": AUTH_CODE,
"redirect_uri": "https://app.example.com/callback",
"code_verifier": PKCE_VERIFIER,
},
)
tokens = res.json()The response:
{
"access_token": "dk_app_at_REDACTED_EXAMPLE_TOKEN",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "dk_app_rt_REDACTED_EXAMPLE_TOKEN",
"scope": "orders:read orders:write",
"install_id": "8b2c1d4e-5f60-4a7b-8c9d-0e1f2a3b4c5d",
"store_id": "3065a1f2-0c4d-4e8b-b7a9-5d2f8c1e9a44",
"store_slug": "sham-perfumes"
}- The access token
dk_app_at_…lives one hour (expires_in: 3600). - The refresh token
dk_app_rt_…rotates on every use; reusing an old one revokes the whole chain. - Every token is bound to exactly one store; two installs on two stores mean two separate tokens.
The full story — the refresh grant, the consent screen, the error table — is in Authorization.
4. First call#
curl "https://dukkan.one/platform-api/v1/orders?limit=5" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"Lists return { "data": [...], "next_cursor": "..." }; fetch one order by id to see the full body:
curl "https://dukkan.one/platform-api/v1/orders/ORDER_ID" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"{
"data": {
"id": "6f1d2c3b-4a59-4e8f-9b70-2d1c3e4f5a6b",
"order_number": "1042",
"status": "placed",
"payment_status": "unpaid",
"source": "storefront",
"line_pricing": "gross",
"subtotal": {
"amount_minor": 250000,
"currency": "SYP",
"decimals": 0
},
"discount": {
"amount_minor": 0,
"currency": "SYP",
"decimals": 0
},
"shipping": {
"amount_minor": 25000,
"currency": "SYP",
"decimals": 0
},
"tax": {
"amount_minor": 0,
"currency": "SYP",
"decimals": 0
},
"total": {
"amount_minor": 275000,
"currency": "SYP",
"decimals": 0
},
"paid": {
"amount_minor": 0,
"currency": "SYP",
"decimals": 0
},
"refunded": {
"amount_minor": 0,
"currency": "SYP",
"decimals": 0
},
"exchange_rate": null,
"created_at": "2026-08-18T16:00:00.123Z",
"updated_at": "2026-08-18T16:00:00.123Z",
"items": [
{
"id": "a1b2c3d4-0001-4e8f-9b70-2d1c3e4f5a6b",
"variant_id": "c0ffee00-0001-4e8f-9b70-2d1c3e4f5a6b",
"name": "عطر العود الملكي — 50 مل",
"quantity": 1,
"unit_price": {
"amount_minor": 250000,
"currency": "SYP",
"decimals": 0
},
"total": {
"amount_minor": 250000,
"currency": "SYP",
"decimals": 0
}
}
],
"fulfillments": [],
"refunds": []
}
}Every amount carries its own decimals, and a Syrian-pound order has none (decimals: 0). Every response carries X-Request-Id and X-Dukkan-Api-Version, and every write requires an Idempotency-Key header. All conventions are in the API reference.
5. Subscribe to webhooks#
Register one endpoint for this install (public HTTPS only). A PUT replaces the whole topic list:
curl -X PUT https://dukkan.one/platform-api/v1/webhooks \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"endpoint_url":"https://app.example.com/hooks/dukkan","topics":["order.created","order.status_changed","order.paid"]}'{
"data": {
"id": "5ub5c41b-0001-4e8f-9b70-2d1c3e4f5a6b",
"endpoint_url": "https://app.example.com/hooks/dukkan",
"topics": [
"order.created",
"order.status_changed",
"order.paid"
],
"status": "active",
"activated_at": "2026-08-18T15:58:00.000Z",
"secret": "dk_whsec_REDACTED_SHOWN_ONCE"
}
}The signing secret appears exactly once — on creation or when rotate_secret: true — so store it immediately.
6. Receive your first event#
Place an order on your sandbox storefront (https://dukkan.one/stores/YOUR_SANDBOX_SLUG) with cash on delivery and watch order.created arrive at your endpoint. Verify the signature before doing anything else; verification samples in several languages are in Webhooks.