---
name: dzbuild-apps
description: Build, test and ship a third-party app for DZBuild stores (dzbuild.com, the e-commerce platform for Algerian merchants). Covers the developer console, the OAuth 2.0 + PKCE install flow at dzbuild.com/oauth/apps, install tokens (dzpk_live_), the REST API at https://api.dzbuild.app/v1 with Idempotency-Key on writes, scopes, webhooks signed with X-DZ-Signature, dz_launch tokens, the WhatsApp order-message API, rate limits, error codes and the review rules. Use whenever a task mentions DZBuild, dzbuild.dev, api.dzbuild.app, a dzapp_ client id or a dzpk_live_ token.
---

# Building a DZBuild app

Every page of https://dzbuild.dev is available as Markdown at the same URL plus `.md`. The index is
https://dzbuild.dev/llms.txt and the whole site is https://dzbuild.dev/llms-full.txt. Read the page
that covers the part you are writing before you write it; the facts below are a summary, not a substitute.

## Facts that decide the design

- The app runs on the developer's own servers. DZBuild runs none of its code.
- Console: https://dzbuild.com/dashboard/developer. Any DZBuild account that owns a store can register up to 10 apps.
- Credentials: `client_id` = `dzapp_` + 20 hex characters (public); client secret = `dzas_` + 48 hex characters (shown once, server side only); signing secret = 64 hex characters (signs webhooks and launch tokens).
- Install flow: OAuth 2.0 authorization code with PKCE, `S256` only. Authorize: `GET https://dzbuild.com/oauth/apps/authorize`. Token: `POST https://dzbuild.com/oauth/apps/token`, form-encoded, `client_secret` in the form or as HTTP Basic.
- The merchant can approve up to 10 of their stores in one consent. The token response carries `access_token`, `token_type` (`Bearer`), `scope`, `store_id`, `install_id` and `stores[]` with one `access_token` per store. Store every entry keyed by `store_id`.
- Install tokens start with `dzpk_live_`, have no `expires_in` and no refresh token. A token works until the merchant uninstalls the app (revoked, calls answer `401`) or installs it again on the same store (replaced). A token reaches its own store only.
- API base: `https://api.dzbuild.app/v1`. JSON. Success `{"data": ..., "meta": {"request_id", "api_version"}}`. Error `{"error": {"code", "message"}, "meta": {...}}`. `GET /v1/whoami` needs no scope and adds `app: {app_id, client_id, install_id}` for install tokens; `GET /v1/ping` needs no token.
- Writes (`POST`, `PATCH`, `DELETE`) require an `Idempotency-Key` header: at most 64 characters from `A-Z a-z 0-9 _ - : .`. The response is stored 24 hours and replayed with `Idempotency-Replay: 1`; the same key with a different method, path or body answers `422 idempotency_key_reuse`; `4xx` answers are stored too, `5xx` and `429` are not.
- Send a `User-Agent` header on every request to DZBuild (token exchange and API); the edge challenges a `POST` without one.
- Rate limits: 120 requests per minute per install (fixed calendar minute), checked before the store's shared budget. `429` carries `error.retry_after` and a `Retry-After` header.
- Webhooks: one URL per app, set in the console, `https` on port 443, public host name, not on a DZBuild domain. Verified once with a `webhook.verify` request (answer any `2xx` within 6 seconds). Headers: `X-DZ-Timestamp`, `X-DZ-Signature: t=<ts>,v1=<hex>`, `X-DZ-Event`, `X-DZ-Delivery`, `User-Agent: DZBuild-Webhooks/1.0`. `v1` = lowercase hex HMAC-SHA256 with the signing secret over `t + "." + raw_body`. Reject when `|now - t| > 300` seconds. Deliveries succeed on a `2xx` within 10 seconds; 5 attempts (waits 60 s, 5 min, 30 min, 2 h); at-least-once, so dedupe on the envelope `id`. After 10 failed attempts in a row the install's endpoint is disabled until Verify is pressed again.
- Webhook URLs must be https on a domain you own. DZBuild refuses IP addresses, localhost, its own domains and *.workers.dev. On workers.dev, poll GET /v1/orders?since=<last created_at>&limit=50 once a minute per store (inside the 120 per minute install budget); status changes need webhooks.
- Events: `order.created`, `order.confirmed`, `order.processing`, `order.shipped`, `order.delivered`, `order.cancelled`, `order.returned` (only with the `orders:read` scope; the payload carries the buyer's name, phone and address), `app.uninstalled` (always), `webhook.verify` (console). App deliveries never carry `X-DZ-Token`.
- Launch link: the merchant's Open button sends the browser to the launch URL with `dz_launch`, a JWT signed HS256 with the signing secret. Claims: `iss` `dzbuild`, `aud` = your `client_id`, `sub` = user id (string), `store_id`, `install_id`, `is_owner`, `iat`, `exp` = `iat` + 300, `jti` (16 hex). Accept only when the signature, `alg`, `iss`, `aud` and `exp` check out; refuse a `jti` seen in the last 5 minutes; then redirect to a URL without the token. The launch URL opened without `dz_launch` must start the install flow.
- The merchant's Install button opens the app's homepage URL, or its launch URL when the homepage is empty. DZBuild never sends the merchant to the authorize URL: the app must start OAuth itself, including when the launch URL is opened without dz_launch.
- App status: `draft` and `in_review` = test mode (new installs only on the developer's own stores; an approved app sent back to `in_review` by an edit keeps every existing install working); `approved`; `rejected` and `suspended` (every call answers `403 app_suspended`). Any store plan can install an app; the developer may set a minimum plan (`403 app_plan_required` below it).
- Closed to install tokens: `/v1/keys`, `/v1/webhooks`, `/v1/changes` (`403`, `Apps cannot use this endpoint`) and the `ai:generate` scope.
- WhatsApp: `whatsapp:read` for `GET /v1/whatsapp/templates`, `GET /v1/whatsapp/balance`, `GET /v1/whatsapp/messages`; `whatsapp:send` for `POST /v1/orders/{id}/whatsapp` with `{"template": "<key>", "language": "ar|fr"}`, answers `202`. Templates are the six Meta-approved order templates (`received`, `confirmed`, `shipped_home`, `shipped_desk`, `delivery_failed`, `desk_ready`, or `shipped` to pick by delivery type). One credit per message from the store's wallet. `402 no_credit`, `409 already_sent`, `422` with the reason as the code.

## Scopes

`analytics:read`, `customers:read`, `delivery:send`, `landing_pages:read`, `landing_pages:write`, `orders:read`, `orders:write`, `pixels:read`, `pixels:write`, `products:read`, `products:write`, `promos:read`, `promos:write`, `shipping:read`, `shipping:write`, `store:read`, `store:write`, `whatsapp:read`, `whatsapp:send`. Request the fewest. `POST`/`PATCH` on orders, products and landing pages need the read scope as well as the write scope. An omitted `scope` parameter grants every scope registered on the app.

## Procedure

1. Register the app in the console: name (3 to 60 characters, not containing "dzbuild"), developer name, one description each in English, Arabic and French, `https` homepage, support email, `https` launch URL, 1 to 5 exact `https` redirect URIs (no fragment, no wildcard), scopes, minimum plan, optional webhook URL and events. Put `client_id`, the client secret and the signing secret in environment variables.
2. Install route: create a PKCE verifier (43 to 128 characters of `A-Z a-z 0-9 - . _ ~`, kept server side) and its `S256` challenge (43 base64url characters), a random `state` tied to the session, and redirect to the authorize URL with `response_type=code`, `client_id`, `redirect_uri`, `scope` (space separated), `state`, `code_challenge`, `code_challenge_method=S256`.
3. Callback route: check `state`; `POST` the token endpoint with `grant_type=authorization_code`, `code`, `redirect_uri` (the same string), `code_verifier`, `client_id`, `client_secret`, and `Accept: application/json`; store each `stores[]` entry encrypted at rest. A code is valid 10 minutes and works once; a failed exchange spends it, so start a new authorize request instead of retrying.
4. Webhook route: read the raw body bytes, parse `X-DZ-Signature`, compute the HMAC, compare in constant time, apply the 5-minute window, dedupe on `id`, answer `200` at once and process the event asynchronously. Press Verify in the console.
5. Launch URL: verify `dz_launch` as above, then use the stored install token for that `store_id` and `install_id`.
6. API calls: `Authorization: Bearer <token>`, `Idempotency-Key` on writes, back off on `429`, treat `401` as an uninstall, stop on `403 app_*` and show the message to the merchant.
7. `app.uninstalled` (or a `401` when no webhook is set): drop the token and delete the store's data within 30 days.
8. Test on your own store while the app is a draft, then submit for review. The console requires: a logo (PNG, JPEG or WebP, at least 128 by 128 pixels, at most 1 MB), the three descriptions of at least 20 characters, a redirect URI, an `https` launch URL, a support email, at least one scope, a verified webhook URL when one is set, and at least one install on your own store.

## Security rules the review checks

- Secrets stay server side: never in a browser, a mobile app, a repository, a log line or a prompt. If one leaks, rotate it in the console.
- Verify `X-DZ-Signature` on every webhook; it is the only proof a request came from DZBuild.
- Exact redirect URIs, a fresh `state` per request, PKCE on every install.
- Reach store data only through the API and webhooks: no dashboard scripting, no merchant passwords or merchant API keys.
- Delete a store's data within 30 days after `app.uninstalled`.
- Ask for the fewest scopes, and say in the listing what the app does with the data.
- Read and answer the support email on the listing.

## Errors to handle

| Answer | Meaning | Do |
|---|---|---|
| `400 bad_request` | Missing or malformed `Idempotency-Key`, invalid JSON, non-numeric id | Fix the request, new key |
| `401 unauthorized` | Bad or revoked token | Treat as uninstalled |
| `402 no_credit` / `quota_exceeded` | The merchant must pay or top up | Do not retry; tell the merchant |
| `403 forbidden` `Missing scope: x` | Scope not granted | Register and request it, reinstall |
| `403 app_uninstalled` / `app_suspended` / `app_not_approved` / `app_plan_required` | App state or store plan | Stop; surface the message |
| `404 not_found` | No such record on this store | |
| `409 already_sent` | WhatsApp template already sent for this order | Do not retry |
| `422 idempotency_key_reuse` | Same key, different request | One key per operation |
| `429 rate_limited` / `too_many_concurrent` | Budget used up | Wait `retry_after`, resend with the same key |
| `502 server_error` | Gateway could not check the token | Retry shortly with the same key |

## Read next

- https://dzbuild.dev/getting-started.md, https://dzbuild.dev/concepts.md
- https://dzbuild.dev/oauth.md, https://dzbuild.dev/scopes.md, https://dzbuild.dev/webhooks.md
- https://dzbuild.dev/rate-limits.md, https://dzbuild.dev/errors.md, https://dzbuild.dev/api-reference.md
- https://dzbuild.dev/whatsapp.md, https://dzbuild.dev/security.md, https://dzbuild.dev/review-guidelines.md
- OpenAPI 3.1: https://dzbuild.dev/openapi/dzbuild-apps-v1.json
