Promo codes
These four endpoints manage the store's promo codes, the codes buyers type at checkout to lower what they pay. They work on the same codes as the promo codes page of the dashboard, described in Discount codes, and they can also rename a code, which the dashboard cannot.
A code takes its discount off the products subtotal, never off shipping. A percentage code takes that share of the subtotal. A fixed code subtracts its amount in DZD and never takes off more than the subtotal. A code cannot be limited to a product, a category or a customer, cannot cap the discount on a large cart, and cannot give free shipping.
Before you start
- The Promo codes add-on must be active on the store (
/dashboard/addons). The list works without it and tells you inaddon_enabled. Every write answers409 addon_inactiveuntil the add-on is on, and buyers cannot use any code at checkout while it is off. - The key needs the promo scopes. Keys created from the dashboard (
/dashboard/api) get both. Scopes are frozen when a key is created, so a key that lacks them answers403 forbidden: create a new key from the dashboard. starts_atandexpires_atare in Algeria time, in the formYYYY-MM-DD HH:MM:SS.
| Scope | Description |
|---|---|
promos:read | Read promo codes. |
promos:write | Create, update and delete promo codes. This changes the prices your buyers pay. |
The promo code object
| Field | Type | Meaning |
|---|---|---|
id | int | The code's id in the store. |
code | string | What buyers type. Upper case, A-Z, 0-9, - and _, unique in the store. |
discount_type | string | percentage or fixed. |
discount_value | number | The percentage (above 0, at most 100) or the amount in DZD. |
min_order_amount | number or null | The products subtotal an order must reach for the code to apply. null means no minimum. |
max_uses | int or null | How many orders can use the code. null means unlimited. |
used_count | int | How many orders have used it. Read-only. |
is_active | bool | An inactive code is refused at checkout. |
starts_at | string or null | Before this time the code is refused. null means it works right away. |
expires_at | string or null | After this time the code is refused. null means it never expires. |
created_at, updated_at | string | YYYY-MM-DD HH:MM:SS, server time. |
GET /v1/promo-codes
The store's codes, newest first. There is no call that reads a single code by id: page through this list.
Auth: platform key with promos:read.
Query parameters
| Param | Type | Default | Notes |
|---|---|---|---|
is_active | string | none | true, 1, yes or on returns the active codes. Any other value returns the inactive ones. Leave it out for all codes. |
limit | int | 50 | 1 to 200. |
cursor | string | none | next_cursor of the previous page. See Pagination. |
Request
curl 'https://api.dzbuild.app/v1/promo-codes?is_active=true' \
-H "Authorization: Bearer $DZ_KEY"
Response 200
{
"data": {
"items": [
{
"id": 20,
"code": "WELCOME10",
"discount_type": "percentage",
"discount_value": 10,
"min_order_amount": 3000,
"max_uses": 100,
"used_count": 7,
"is_active": true,
"starts_at": null,
"expires_at": "2026-11-30 23:59:00",
"created_at": "2026-10-06 14:20:11",
"updated_at": "2026-10-06 14:20:11"
}
],
"next_cursor": null,
"has_more": false,
"addon_enabled": true
}
}
addon_enabled says whether the Promo codes add-on is active. When it is false, the list still answers, but writes fail and buyers cannot use the codes.
POST /v1/promo-codes
Creates a code. It works at checkout as soon as it is created, unless you send is_active: false or a later starts_at.
Auth: platform key with promos:write. Requires Idempotency-Key.
Body
| Field | Type | Required | Notes |
|---|---|---|---|
code | string | yes | Spaces around it are removed and letters are upper-cased, then it must be 2 to 30 characters of A-Z, 0-9, - or _. A code that already exists in the store answers 409 code_exists. |
discount_type | string | yes | percentage or fixed, in any case. There is no default. |
discount_value | number | yes | Above 0. At most 100 for percentage, and 100 needs the merchant's confirmation (see the section on 100% discounts below). No upper limit for fixed. |
min_order_amount | number or null | no | Minimum products subtotal in DZD. 0, "" or null means no minimum. |
max_uses | int or null | no | 1 or more. 0, "" or null means unlimited. |
is_active | bool | no | Defaults to true. Send a JSON boolean: a string such as "false" is read as true. |
starts_at | string or null | no | A common date-time form, such as 2026-11-01 08:00 or an ISO 8601 string. Stored as YYYY-MM-DD HH:MM:SS in Algeria time; a value with a UTC offset is converted. null or "" means no start date. |
expires_at | string or null | no | Same forms. It must be in the future and after starts_at. null or "" means no expiry. |
confirm_token | string | no | Only for a 100% discount. |
confirm_full_discount | bool | no | Only for a 100% discount. |
Request
curl -X POST 'https://api.dzbuild.app/v1/promo-codes' \
-H "Authorization: Bearer $DZ_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: promo-welcome10-create" \
-d '{"code": "welcome10", "discount_type": "percentage", "discount_value": 10, "min_order_amount": 3000, "max_uses": 100, "expires_at": "2026-11-30 23:59"}'
Response 201
{
"data": {
"id": 20,
"code": "WELCOME10",
"discount_type": "percentage",
"discount_value": 10,
"min_order_amount": 3000,
"max_uses": 100,
"used_count": 0,
"is_active": true,
"starts_at": null,
"expires_at": "2026-11-30 23:59:00",
"created_at": "2026-10-06 14:20:11",
"updated_at": "2026-10-06 14:20:11"
}
}
PATCH /v1/promo-codes/{id}
Changes only the fields you send, with the same rules as the create. A field you leave out keeps its value.
Auth: platform key with promos:write. Requires Idempotency-Key.
codecan be changed. The new text must be free in the store, otherwise the call answers409 code_exists.discount_typeanddiscount_valueare checked as a pair. Sending onlydiscount_type: "percentage"on a 500 DZDfixedcode answers422 invalid_discount_value, because 500 is above 100. Send both.- A new
expires_atmust be in the future. A code that has already expired stays editable as long as you do not sendexpires_at. used_countcannot be written.- An empty body changes nothing and returns the code.
- A code of another store answers
404, like a code that does not exist.
Request
curl -X PATCH 'https://api.dzbuild.app/v1/promo-codes/20' \
-H "Authorization: Bearer $DZ_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: promo-20-extend" \
-d '{"max_uses": 200, "expires_at": "2026-12-31 23:59"}'
Response 200
{
"data": {
"id": 20,
"code": "WELCOME10",
"discount_type": "percentage",
"discount_value": 10,
"min_order_amount": 3000,
"max_uses": 200,
"used_count": 7,
"is_active": true,
"starts_at": null,
"expires_at": "2026-12-31 23:59:00",
"created_at": "2026-10-06 14:20:11",
"updated_at": "2026-10-20 09:05:42"
}
}
DELETE /v1/promo-codes/{id}
Deletes the code, so buyers can no longer use it. Orders already placed with it keep their discount. To pause a code without losing its usage count, send is_active: false with PATCH instead.
Auth: platform key with promos:write. Requires Idempotency-Key.
Request
curl -X DELETE 'https://api.dzbuild.app/v1/promo-codes/20' \
-H "Authorization: Bearer $DZ_KEY" \
-H "Idempotency-Key: promo-20-delete"
Response 200
{
"data": {
"deleted": true,
"id": 20
}
}
100% discounts
A percentage code at 100 makes the goods free for every buyer who has the code. A create, or an update that sends discount_type or discount_value, is not written on the first call when the result is a 100% percentage code:
- The call answers
422 confirmation_requiredand writes nothing. The error carriesconfirm_token(single use, valid600seconds),actionandwill_change, a summary to show the merchant. - Once the merchant approves, repeat the same body with
confirm_tokenadded and a newIdempotency-Key(the first key is bound to the body without the token). A token that expired, was already used, or no longer matches the request answers422 confirmation_stalewith a fresh token.
With a key created from the dashboard you can skip the round trip by sending confirm_full_discount: true in the first call. The DZBuild Copilot cannot use that flag and always goes through the token.
{
"error": {
"code": "confirmation_required",
"message": "A 100% discount makes every order free ...",
"confirm_token": "cft_xxxxxxxxxxxxxxxx",
"confirm_token_expires_in": 600,
"action": "promo.full_discount:new",
"will_change": {
"action": "Create a promo code that makes orders free",
"code": "FREEGIFT",
"discount": "100% off the whole order subtotal",
"reversible": true,
"note": "Any customer with this code pays 0 for the goods. Orders already placed with it cannot be reversed by deleting the code."
}
}
}
On an update, action ends with the code's id instead of new, and will_change.action reads Change promo code #20 to make orders free.
curl -X POST 'https://api.dzbuild.app/v1/promo-codes' \
-H "Authorization: Bearer $DZ_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: promo-freegift-approved" \
-d '{"code": "FREEGIFT", "discount_type": "percentage", "discount_value": 100, "max_uses": 1, "confirm_token": "cft_REPLACE_WITH_TOKEN"}'
Change history and undo
Every create, update and delete made through the API is recorded. Codes changed on the dashboard page are not recorded, so they cannot be undone through the API.
GET /v1/changes?entity=promo_codelists the promo code changes, newest first, withstore:read. Each item carries the changeid, the code's id asentity_id, theaction(create,updateordelete) and asummary.POST /v1/changes/{id}/undoneedspromos:write, anIdempotency-Keyand the add-on active, like any write.- Undoing an update puts the earlier values back, with no confirmation, even a 100% discount or an expiry date that has passed. If the code was deleted since, the undo answers
422 restore_target_missing. - Undoing a delete re-creates the code with its usage count, and keeps its id when that id is still free.
- Undoing a create answers
422 nothing_to_restore: delete the code instead. - An installed app's token cannot read or undo changes: both answer
403 forbidden.
curl -X POST 'https://api.dzbuild.app/v1/changes/500/undo' \
-H "Authorization: Bearer $DZ_KEY" \
-H "Idempotency-Key: undo-500"
{
"data": {
"undone": true,
"change_id": 500,
"entity": "promo_code",
"undo_change_id": 510
}
}
A change undone a second time answers 409 already_undone.
Errors
| HTTP | Code | Cause |
|---|---|---|
| 400 | bad_request | The id in the path is not all digits, the body is not valid JSON, or Idempotency-Key is missing or malformed. |
| 403 | forbidden | Missing scope: promos:read or Missing scope: promos:write, or API access requires an active Enterprise plan for a merchant key whose store is not on an active Enterprise plan. |
| 404 | not_found | No promo code with this id in the store. |
| 409 | addon_inactive | The Promo codes add-on is not active on the store. Writes only. |
| 409 | code_exists | Another code of the store already has this text. |
| 422 | invalid_code | code is shorter than 2 or longer than 30 characters, or has a character other than A-Z, 0-9, - and _. |
| 422 | invalid_discount_type | discount_type is missing, or is not percentage or fixed. |
| 422 | invalid_discount_value | discount_value is missing, not a number, 0 or less, or above 100 for percentage. |
| 422 | invalid_min_order_amount | min_order_amount is not a number. |
| 422 | invalid_max_uses | max_uses is not a number, or is below 1. |
| 422 | invalid_starts_at, invalid_expires_at | The date cannot be read. |
| 422 | expires_at_in_past | The expires_at you sent is not in the future. |
| 422 | invalid_date_window | expires_at is not after starts_at. |
| 422 | confirmation_required, confirmation_stale | A 100% discount waits for the merchant's approval. See the section above. |
| 422 | idempotency_key_reuse | The same Idempotency-Key was used with a different body or path. |
| 500 | server_error | The write failed. Retry with the same Idempotency-Key. |
A 4xx answer is stored with its Idempotency-Key for 24 hours and replayed to a retry with the same body. After you activate the add-on or fix the body, send the call with a new key. Errors that every endpoint shares, such as 401, 402 and 429, are in Errors, and the retry rules are in Idempotency.
Known limits
- Uses can pass
max_uses. When several buyers check out with the same code at the same moment,used_countcan end abovemax_uses. - No webhook. Creating, changing or deleting a promo code sends no webhook event.