# DZBuild API

Build your own DZBuild apps

Build apps that merchants can install on their DZBuild stores. Visit the [Developer Portal at dzbuild.dev](https://dzbuild.dev/) for official starters and guides, or [create an app](https://dzbuild.dev/apps/new).

The Enterprise requirement below applies to personal merchant API keys. App installations follow separate [access rules](https://dzbuild.dev/concepts), including the app's minimum plan and permissions.

The DZBuild API lets you manage your store programmatically: products, orders, customers, landing pages, plus a high-volume **signup tracker** for merchants embedding DZBuild in their own platforms.

**Base URL:** `https://api.dzbuild.app/v1` — the only supported public base URL. **Versioning:** stable `v1`. Breaking changes require a new path prefix. **Format:** JSON in, JSON out, UTF-8. **Authentication:** see [Authentication](https://dzbuild.com/api-docs/authentication.md). **Personal API keys:** require a store with an active Enterprise plan. This requirement does not apply to app installation tokens, which follow the access rules linked above. **Status:** Pilot. The owner of a store on an active Enterprise plan creates personal keys in the dashboard at **Settings → API** (`/dashboard/api`). A new key is enrolled in the pilot automatically and works at once, so there is nothing to request from support.

Point your integration at `api.dzbuild.app`, nothing else

`https://dzbuild.com/api/v1` is an internal alias, not an integration target. On that hostname: `/v1/signups` and `/v1/events` do not exist (`404`), public-key (`DZ-Public`) calls are rejected with `401`. Idempotent replay and the per-store rate limit apply on both hosts.

## Quickstart

```
curl https://api.dzbuild.app/v1/ping
```

Response:

```
{ "data": { "pong": true, "time": "2026-04-30T20:15:59.836Z", "edge": true },

  "meta": { "request_id": "...", "api_version": "v1", "edge": true } }
```

Authenticated request:

```
curl https://api.dzbuild.app/v1/whoami \

  -H "Authorization: Bearer <your_key_id>.<your_key_secret>"
```

Response:

```
{ "data": { "key_id": "dzpk_live_xxxxxxxxxxxxxx", "store_id": 10, "type": "platform",

            "rate_limit_tier": "enterprise", "pilot": true,

            "scopes": ["store:read", "store:write", "products:read", "products:write",

                       "orders:read", "orders:write", "customers:read", "landing_pages:read",

                       "landing_pages:write", "promos:read", "promos:write", "pixels:read",

                       "pixels:write", "shipping:read", "shipping:write", "webhooks:read",

                       "webhooks:write", "usage:read", "analytics:read", "whatsapp:read",

                       "whatsapp:send"] },

  "meta": { "request_id": "...", "api_version": "v1" } }
```

`scopes` lists what this key may call: an endpoint that needs a scope missing from the list answers `403` with `Missing scope:` and the scope's name. Called with an installed app's token, `whoami` also returns `app`, with its `app_id`, `client_id` and `install_id`.

## Envelope

Every response follows the same shape:

**Success**

```
{ "data": ..., "meta": { "request_id": "...", "api_version": "v1" } }
```

**Error**

```
{ "error": { "code": "rate_limited", "message": "...", "retry_after": 12 },

  "meta": { "request_id": "...", "api_version": "v1" } }
```

## Response headers

| Header               | When                                                                           | Meaning                                                                                                                                                                                                                    |
| -------------------- | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `X-Request-Id`       | Every response                                                                 | Same value as `meta.request_id`, except on an idempotent replay: its body is the stored one and keeps the first call's `meta.request_id`. Send your own `X-Request-Id` and we echo it back, so your logs and ours line up. |
| `X-Api-Version`      | Most responses                                                                 | Always `v1`.                                                                                                                                                                                                               |
| `X-Cache`            | `GET /v1/products`, `GET /v1/landing-pages`, `GET /v1/store` and its sub-paths | Always `MISS`: these reads are served fresh.                                                                                                                                                                               |
| `Idempotency-Replay` | Replayed writes                                                                | `1` means this is the stored response of an earlier call with the same `Idempotency-Key` — no new side effect happened. See [Idempotency](https://dzbuild.com/api-docs/idempotency.md).                                    |

## Good to know

* Your key is verified on every single request — Bearer tokens and public-key HMAC signatures alike.
* The per-minute rate limit applies **per store** — all of a store's keys share one budget — on a fixed 60-second window (the counter resets on each wall-clock minute). Enforcement is approximate under bursts, so handle `429` defensively rather than pacing exactly to the limit. It applies to API calls only — a merchant's storefront, checkout and dashboard never consume it. See [Rate limits](https://dzbuild.com/api-docs/rate-limits.md).
* Every `GET` is served fresh from the platform: there is no edge read cache on `api.dzbuild.app`, so `GET /v1/products`, `GET /v1/landing-pages`, `GET /v1/store` and every `GET` under it return the current values on each call. Cache on your side when you read at scale.
* High-volume writes (`/v1/signups`, `/v1/events`) are accepted asynchronously and return `202 Accepted` immediately — your call doesn't wait for processing to finish.
