Skip to main content

Promo codes

List, create, update and delete a store's promo codes from your own code, with the add-on rule, the 100% discount confirmation and undo.

Written by Support

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 in addon_enabled. Every write answers 409 addon_inactive until 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 answers 403 forbidden: create a new key from the dashboard.

  • starts_at and expires_at are in Algeria time, in the form YYYY-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.

  • code can be changed. The new text must be free in the store, otherwise the call answers 409 code_exists.

  • discount_type and discount_value are checked as a pair. Sending only discount_type: "percentage" on a 500 DZD fixed code answers 422 invalid_discount_value, because 500 is above 100. Send both.

  • A new expires_at must be in the future. A code that has already expired stays editable as long as you do not send expires_at.

  • used_count cannot 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:

  1. The call answers 422 confirmation_required and writes nothing. The error carries confirm_token (single use, valid 600 seconds), action and will_change, a summary to show the merchant.

  2. Once the merchant approves, repeat the same body with confirm_token added and a new Idempotency-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 answers 422 confirmation_stale with 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_code lists the promo code changes, newest first, with store:read. Each item carries the change id, the code's id as entity_id, the action (create, update or delete) and a summary.

  • POST /v1/changes/{id}/undo needs promos:write, an Idempotency-Key and 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_count can end above max_uses.

  • No webhook. Creating, changing or deleting a promo code sends no webhook event.

Did this answer your question?