Skip to content

Build an app in 15 minutes

From registering the app to the first signed webhook on a sandbox store.

Updated 2 Sept 20262 min read
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#

  1. Sign in to the developer portal and enroll your organization. This requires the organization owner and a verified email.
  2. From the dashboard create an app: a name, at least one exact redirect URI (HTTPS only; localhost is allowed for development), only the scopes the app actually needs, and the webhook topics it will subscribe to.
  3. 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:

HTTP
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=S256

After consent, a code arrives on your redirect URI together with the same state. Exchange it for tokens:

Shell
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_VERIFIER

The response:

token-response.jsonJSON
{
  "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#

Shell
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:

Shell
curl "https://dukkan.one/platform-api/v1/orders/ORDER_ID" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
order.jsonJSON
{
  "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:

Shell
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"]}'
webhook-subscription-response.jsonJSON
{
  "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.

Next steps#