Shipping
These endpoints cover what the dashboard keeps on its shipping pages: the home and desk delivery price of each wilaya, the free-shipping rules, and the couriers linked to the store. They also serve the wilaya and commune lists a checkout form needs, and the communes and stop desks the store's courier serves.
Prices are in DZD. POST /v1/orders prices delivery from these rates and ignores any shipping cost sent in the body, so a custom storefront reads the rates to show an estimate and lets the order compute the charge. See Custom themes & storefronts.
Before you start
- The key needs the shipping scopes. Keys created from the dashboard (Settings → API,
/dashboard/api) have both. Scopes are frozen when a key is created, so an older key that lacks them answers403 forbidden: create a new key from the dashboard. - A store that sells digital products has no shipping setup: every write on this page answers
422 shipping_not_availablethere. - Every write needs an
Idempotency-Keyheader. See Idempotency. - Testing and linking a courier call the courier's servers during the request, and a rate sync calls them in the background. These three calls and
POST /v1/orders/{id}/send-to-deliveryshare a per-store courier budget on top of the rate limits. - Rate and settings writes can be undone. Linking, unlinking and changing the default courier cannot. The undo section at the end of this page explains how.
- Shipping reads are not cached at the edge: a
GETsent right after a write returns the new values.
| Scope | Description |
|---|---|
shipping:read | Read shipping rates and settings, linked couriers, courier coverage and the wilaya and commune lists. |
shipping:write | Change shipping rates and settings, and link, test, unlink or sync couriers. |
GET /v1/wilayas
The wilayas the store delivers to, following its wilaya mode: 1 to 58 in the courier-compatible mode, 1 to 69 in 69-wilaya mode. Names come in Arabic, French and English. The whole list comes in one answer, without pagination.
Auth: platform key with shipping:read.
Request
curl 'https://api.dzbuild.app/v1/wilayas' \
-H "Authorization: Bearer $DZ_KEY"
Response 200
Two of the 58 wilayas are shown. mode_note is one English sentence about the mode.
{
"data": {
"wilaya_mode": "58",
"mode_note": "Courier-compatible mode: wilayas 1-58 only.",
"count": 58,
"wilayas": [
{ "id": 1, "name_ar": "أدرار", "name_fr": "Adrar", "name_en": "Adrar" },
{ "id": 16, "name_ar": "الجزائر", "name_fr": "Alger", "name_en": "Algiers" }
]
}
}
GET /v1/wilayas/{id}/communes
The communes of one wilaya, sorted by French name. Any wilaya from 1 to 69 is answered, whatever the store's wilaya mode. The whole list comes in one answer.
Auth: platform key with shipping:read.
This is the platform's own commune list. It does not say which communes a courier serves: GET /v1/shipping/coverage does.
Request
curl 'https://api.dzbuild.app/v1/wilayas/16/communes' \
-H "Authorization: Bearer $DZ_KEY"
Response 200
Two of the 57 communes of wilaya 16 are shown.
{
"data": {
"wilaya_id": 16,
"count": 57,
"communes": [
{ "id": 564, "wilaya_id": 16, "name_ar": "عين بنيان", "name_fr": "Ain Benian" },
{ "id": 558, "wilaya_id": 16, "name_ar": "عين طاية", "name_fr": "Ain Taya" }
]
}
}
An id that is not all digits answers 400 bad_request. A wilaya that does not exist answers 404 not_found.
GET /v1/shipping/rates
The delivery price of every wilaya the store has a rate for, keyed by wilaya id. A wilaya without a rate is absent from rates.
Auth: platform key with shipping:read.
Request
curl 'https://api.dzbuild.app/v1/shipping/rates' \
-H "Authorization: Bearer $DZ_KEY"
Response 200
One wilaya is shown.
{
"data": {
"wilaya_mode": "58",
"currency": "DZD",
"limits": {
"max_price": 100000,
"max_delivery_days": 60
},
"count": 58,
"rates": {
"16": {
"home_price": 400,
"home_enabled": true,
"desk_price": 300,
"desk_enabled": true,
"days": 1,
"is_active": true,
"synced_provider": null,
"synced_at": null
}
}
}
}
| Field | Meaning |
|---|---|
wilaya_mode | 58 or 69, the same value as in GET /v1/shipping/settings. |
limits | The highest price and the highest days that POST /v1/shipping/rates accepts. |
count | Number of wilayas in rates. |
home_price, desk_price | Price of home delivery and of desk delivery, in DZD. |
home_enabled, desk_enabled | Whether the store offers that delivery type in this wilaya. |
days | Delivery time in days. |
synced_provider, synced_at | The courier whose price list last wrote this rate, and when (YYYY-MM-DD HH:MM:SS). null when no courier sync wrote it. |
POST /v1/shipping/rates
Creates or changes the rates of the wilayas you send. The other wilayas are not touched.
Auth: platform key with shipping:write. Requires Idempotency-Key.
Body
rates is an object keyed by wilaya id, written as plain digits ("16", not "016"), with 1 to 69 wilayas. Each value holds the fields to set, and every field is optional.
| Field | Type | Notes |
|---|---|---|
home_price | number | DZD, rounded to two decimals, 0 to 100000. A numeric string is accepted. |
home_enabled | bool | true or false. 0, 1, "0" and "1" are accepted too. |
desk_price | number | Same rules as home_price. |
desk_enabled | bool | Same rules as home_enabled. |
days | int | A whole number of days, 0 to 60. |
A field you leave out, or send as null, keeps its stored value. A wilaya that had no rate starts from price 0, both delivery types on and 3 days.
Request
curl -X POST 'https://api.dzbuild.app/v1/shipping/rates' \
-H "Authorization: Bearer $DZ_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: rates-2026-10-06-1" \
-d '{"rates": {"16": {"home_price": 450, "desk_price": 350}, "31": {"desk_enabled": false}}}'
Response 200
rates holds the written wilayas only, as stored after the write.
{
"data": {
"updated": 2,
"wilaya_ids": [16, 31],
"rates": {
"16": { "home_price": 450, "home_enabled": true, "desk_price": 350, "desk_enabled": true, "days": 1 },
"31": { "home_price": 500, "home_enabled": true, "desk_price": 350, "desk_enabled": false, "days": 2 }
}
}
}
The prior values are saved, so the write can be undone. The answer does not carry a change_id: see the undo section.
GET /v1/shipping/settings
The free-shipping rules and the wilaya mode.
Auth: platform key with shipping:read.
Request
curl 'https://api.dzbuild.app/v1/shipping/settings' \
-H "Authorization: Bearer $DZ_KEY"
Response 200
The answer also carries a notes object with two English sentences that restate the threshold rule and the wilaya mode rule. The example leaves it out.
{
"data": {
"free_shipping": false,
"free_shipping_threshold": 8000,
"free_shipping_threshold_active": true,
"wilaya_mode": "58"
}
}
| Field | Meaning |
|---|---|
free_shipping | true when delivery is free on every order. |
free_shipping_threshold | Order subtotal in DZD from which delivery is free. 0 or null means no threshold. |
free_shipping_threshold_active | true only when the threshold is above 0. |
wilaya_mode | "58": the 58 wilayas couriers work with. "69": all 69 wilayas, a mode set from the dashboard. |
PATCH /v1/shipping/settings
Changes one or more of the three settings. Send at least one; other fields are ignored.
Auth: platform key with shipping:write. Requires Idempotency-Key.
Body
| Field | Type | Notes |
|---|---|---|
free_shipping | bool | true or false. 0, 1, "0" and "1" are accepted too. |
free_shipping_threshold | number or null | DZD, rounded to two decimals, 0 to 99999999.99. 0 or null turns the threshold off. |
wilaya_mode | string | Only "58", which moves a store in 69-wilaya mode back to 58 wilayas. Switching to 69 wilayas is done from the shipping rates page of the dashboard (/dashboard/shipping) and answers 422 wilaya_mode_69_unsupported here. |
Request
curl -X PATCH 'https://api.dzbuild.app/v1/shipping/settings' \
-H "Authorization: Bearer $DZ_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: settings-2026-10-06-1" \
-d '{"free_shipping_threshold": 8000}'
Response 200
The settings after the write, without notes. The prior values are saved, so the change can be undone.
GET /v1/shipping/providers
Every courier the platform supports, linked to the store or not, with what each one asks for when you link it. Credential values are never returned: has_id and has_token only say whether one is stored. The whole list comes in one answer.
Auth: platform key with shipping:read.
Request
curl 'https://api.dzbuild.app/v1/shipping/providers' \
-H "Authorization: Bearer $DZ_KEY"
Response 200
One courier is shown. The answer also carries a note sentence, left out here.
{
"data": {
"count": 103,
"providers": [
{
"provider": "yalidine",
"family": "yalidine",
"credentials": {
"api_id": { "label": "API ID", "required": true },
"api_token": { "label": "API Token", "required": true },
"_note": "Send an empty string to keep the currently stored value. Credentials are never returned by this API."
},
"extra_fields": {
"delivery_tier": {
"label": "Service tier",
"required": false,
"values": ["express"],
"note": "Only \"express\" is valid here: this courier rejects the economic parameter outright and every order push would fail."
}
},
"supports_rate_sync": true,
"linked": true,
"source": "store_delivery_providers",
"is_enabled": true,
"is_default": true,
"is_send_default": true,
"has_id": true,
"has_token": true,
"delivery_tier": "express",
"economic_available": null,
"credentials_failed_at": null,
"synced_tier": "express",
"stock_account": null,
"auto_validate": null,
"custom_name": null,
"linked_at": "2026-09-14 10:12:00",
"updated_at": "2026-09-14 10:12:00"
}
]
}
}
| Field | Meaning |
|---|---|
family | yalidine, procolis, ecotrack or standalone. |
credentials | What api_id and api_token mean for this courier, in the courier's own words. api_token is absent for a courier that takes one value. |
extra_fields | The other fields this courier accepts when you link it, keyed by name. An empty array when there are none. |
supports_rate_sync | Whether POST /v1/shipping/rates/sync works with this courier. |
linked | Whether the store has this courier. |
source | store_delivery_providers for a courier linked from the courier list (this API or the dashboard), store_row for a courier set up the older way, directly in the store settings, null when not linked. |
is_enabled | Whether the link is turned on. |
is_default | Whether this is the store's default courier. |
is_send_default | The courier POST /v1/orders/{id}/send-to-delivery uses when the call names none. |
delivery_tier, synced_tier, economic_available | Yalidine-family service tier: the one chosen, the one the last rate sync used, and whether the account offered the economic tier at that sync. |
stock_account, auto_validate, custom_name | The extra fields stored for this courier, null when not set. |
credentials_failed_at | ISO 8601 time, set when the courier kept refusing the stored credentials. Sends to this courier are refused while it is set. Linking the courier again clears it. |
linked_at, updated_at | YYYY-MM-DD HH:MM:SS. null for a courier set in the store settings. |
What api_id and api_token hold
provider | api_id | api_token |
|---|---|---|
yalidine, yalitec, guepex, easyandspeed, economiqua, wecan | API ID | API Token |
zrexpress, abexexpress, leopardexpress, colilog, flashdelivery | Token | Key |
zrexpressnew | API Key (secret key) | Tenant ID |
noest | API Token | User GUID |
colivraison | Public Key | Bearer Token |
ecomdelivery | API Key | API Token |
neardelivery | ApiKey | ApiSecret |
maystro | API Token | none |
zimou | Bearer Token | none |
elogistia | API Key | none |
mdm | x-api-key | none |
customecotrack and every courier of the ecotrack family | Bearer Token | none |
Extra fields, all optional unless stated:
delivery_tier, Yalidine family:express.guepexalso takeseconomic.stock_account,ecotrackfamily: fulfil orders from the courier's stock.auto_validate,noest: validate orders automatically at the courier.api_urlandcustom_name,customecotrack, both required to link: the courier's https Ecotrack address (a host ending in.ecotrack.dz, orplatform.dhd-dz.comorapp.conexlog-dz.com) and the name to show for it, up to 100 characters.
POST /v1/shipping/providers/test
Sends credentials to the courier and reports whether it accepted them. Nothing is saved. An empty or missing api_id or api_token uses the value stored for this courier, so you can re-test a linked courier without holding its credentials.
Auth: platform key with shipping:write. Requires Idempotency-Key. Counts against the courier budget.
Body
| Field | Type | Required | Notes |
|---|---|---|---|
provider | string | yes | A slug from GET /v1/shipping/providers. |
api_id | string | unless stored | First credential. |
api_token | string | unless stored | Second credential, for couriers that take two. |
api_url | string | customecotrack only | The courier's Ecotrack address. When the stored credentials are reused, it must match the stored address. |
delivery_tier | string | no | express or economic. economic is accepted for guepex only. |
Request
curl -X POST 'https://api.dzbuild.app/v1/shipping/providers/test' \
-H "Authorization: Bearer $DZ_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: test-yalidine-1" \
-d '{"provider": "yalidine", "api_id": "YOUR_API_ID", "api_token": "YOUR_API_TOKEN"}'
Response 200
A courier that refuses the credentials also answers 200, with ok: false and the courier check's message. resolved_provider is set for zrexpressnew only: the ZR Express platform that accepted the pair.
{
"data": {
"provider": "yalidine",
"ok": true,
"message": "تم الاتصال بنجاح",
"resolved_provider": null,
"saved": false
}
}
POST /v1/shipping/providers
Links a courier, or re-saves a linked one. The platform tests the credentials with the courier first and saves nothing when the courier refuses them.
Auth: platform key with shipping:write. Requires Idempotency-Key. Counts against the courier budget.
Body
The fields of the test call, plus:
| Field | Type | Default | Notes |
|---|---|---|---|
enabled | bool | true | Turn the link on or off. |
set_default | bool | false | Make this courier the store's default. |
custom_name | string | none | customecotrack only, required there. HTML tags are removed and the name is cut to 100 characters. |
stock_account | bool | stored value | ecotrack family. |
auto_validate | bool | stored value | noest. |
- An empty
api_idorapi_tokenkeeps the stored value, so a linked courier can be re-saved without sending its credentials again. - The first courier a store links becomes its default. In the answer,
is_defaultistrueonly when this call made the courier the default, so a default courier re-saved withoutset_defaultstays the default while the answer saysfalse.GET /v1/shipping/providersshows the real state. zrexpressandzrexpressneware one courier: linking one replaces the other. Azrexpressnewpair accepted by the older ZR Express platform is saved aszrexpress, andproviderin the answer says so.
Request
curl -X POST 'https://api.dzbuild.app/v1/shipping/providers' \
-H "Authorization: Bearer $DZ_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: link-yalidine-1" \
-d '{"provider": "yalidine", "api_id": "YOUR_API_ID", "api_token": "YOUR_API_TOKEN", "set_default": true}'
Response 200
{
"data": {
"provider": "yalidine",
"is_enabled": true,
"is_default": true,
"has_id": true,
"has_token": true,
"undoable": false,
"note": "Courier credentials are never recorded, so linking cannot be undone. To revert, link the previous courier again or unlink this one."
}
}
A refused credential answers 422 credentials_rejected with the courier's message, and nothing is saved.
POST /v1/shipping/providers/default
Makes a linked courier the store's default. New sends to delivery go to it.
Auth: platform key with shipping:write. Requires Idempotency-Key.
provider in the body names the courier. Only a courier linked from the courier list can be made the default; a courier set in the store settings answers 404 provider_not_linked. When the chosen courier is turned off, warning says that sends stay off until it is turned on again.
Request
curl -X POST 'https://api.dzbuild.app/v1/shipping/providers/default' \
-H "Authorization: Bearer $DZ_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: default-noest-1" \
-d '{"provider": "noest"}'
Response 200
{
"data": {
"provider": "noest",
"is_default": true,
"previous_default": "yalidine",
"undoable": false,
"warning": "New send-to-delivery pushes now go to \"noest\". "
}
}
Confirmation for a rate sync or an unlink
A rate sync overwrites the merchant's own prices and an unlink removes stored credentials, so both calls ask for a confirmation before they act.
- Call without a confirmation. The answer is
422 confirmation_required, and theerrorobject addsconfirm_token(single use),confirm_token_expires_in(600seconds),actionandwill_change, the summary to show the merchant. - Once the merchant approves, repeat the call with
confirm_tokenin the body and a newIdempotency-Key. The first key is bound to the body without the token, so reusing it answers422 idempotency_key_reuse.
A token works once, only for the key that received it, and only while what it describes is unchanged: the rate table for a sync, and for an unlink the number of linked couriers and whether this one is the default. A token that was used, expired or no longer matches answers 422 confirmation_stale with a fresh token and summary.
A key that is not used by the in-dashboard assistant may send "confirm": true instead of a token and skip step 1. Keys used by the assistant must send the token.
A sync's first answer looks like this.
{
"error": {
"code": "confirmation_required",
"message": "Syncing overwrites your own prices for every wilaya \"yalidine\" serves. Show the merchant the summary below; when they approve, re-send with the confirm_token.",
"confirm_token": "cft_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"confirm_token_expires_in": 600,
"action": "shipping.rates_sync:yalidine",
"will_change": {
"action": "Overwrite shipping rates from yalidine",
"wilayas_at_risk": 58,
"reversible": true,
"note": "The prior prices are saved to the change log first, so this can be undone."
}
}
}
For an unlink, will_change holds action, was_store_default, remaining_providers, consequence, reversible (false) and note.
POST /v1/shipping/rates/sync
Replaces the store's prices with the courier's own price list, for each wilaya from 1 to 58 the courier prices. The sync runs in the background. Before anything is queued, the whole rate table is saved, and change_id in the answer undoes the sync.
Auth: platform key with shipping:write. Requires Idempotency-Key and a confirmation. Counts against the courier budget.
Body
| Field | Type | Required | Notes |
|---|---|---|---|
provider | string | yes | A courier linked from the courier list and turned on. mdm and neardelivery have no price list. |
confirm_token | string | see above | From the confirmation_required answer. |
confirm | bool | see above | true, for keys not used by the assistant. |
What the sync changes
- It writes
home_priceanddesk_priceand setssynced_providerandsynced_at. The on/off switches anddaysof a wilaya that already had a rate stay as they were. A wilaya that had no rate gets3days. - Yalidine-family couriers need the store's wilaya, set with
wilaya_idinPATCH /v1/store(see Store). Without it the call answers422 store_wilaya_required. - Follow the result with
GET /v1/shipping/rates: the rates the sync wrote name the courier insynced_providerand carry a newsynced_at. When the courier sends no prices, the rates stay as they were. - While a sync of the same courier is running, the call answers
202withstatus: already_running, that sync'ssync_idandchange_id: null, without asking for a confirmation. - After a sync succeeds, the same courier can be synced again 5 minutes later. An earlier call answers
429 sync_cooldown.
Request
curl -X POST 'https://api.dzbuild.app/v1/shipping/rates/sync' \
-H "Authorization: Bearer $DZ_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: sync-yalidine-2" \
-d '{"provider": "yalidine", "confirm_token": "cft_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}'
Response 202
{
"data": {
"status": "queued",
"sync_id": "a1b2c3d4e5f6a7b8c9d0e1f2",
"change_id": 500,
"note": "The sync runs in the background and overwrites your prices for every wilaya this courier serves. Poll GET /v1/shipping/rates for the result; undo change_id to restore the prior prices."
}
}
DELETE /v1/shipping/providers/{provider}
Unlinks a courier and removes its credentials from the store. This cannot be undone: to send with that courier again, link it again. Parcels already at the courier keep being tracked.
Auth: platform key with shipping:write. Requires Idempotency-Key and a confirmation.
providerin the path is the courier's slug. Only a courier linked from the courier list can be unlinked here; any other answers404 provider_not_linked.- The body carries only the confirmation:
confirm_token, orconfirm: truefor keys not used by the assistant. - When the unlinked courier was the default, the other courier that is turned on and was linked first becomes the default.
- When there is none, no courier is the default and sends to delivery stop for the whole store. The answer then has
new_default: nullandsend_to_delivery_active: false.
Request
curl -X DELETE 'https://api.dzbuild.app/v1/shipping/providers/yalidine' \
-H "Authorization: Bearer $DZ_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: unlink-yalidine-2" \
-d '{"confirm_token": "cft_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}'
Response 200
{
"data": {
"provider": "yalidine",
"unlinked": true,
"undoable": false,
"remaining_providers": 1,
"new_default": "noest",
"send_to_delivery_active": true,
"warning": "The store default is now \"noest\"; new send-to-delivery pushes go there."
}
}
GET /v1/shipping/coverage
Which wilayas, communes and stop desks a linked courier serves, from the courier's own data. A courier set in the store settings counts as linked here.
Auth: platform key with shipping:read.
Query parameters
| Param | Type | Default | Notes |
|---|---|---|---|
provider | string | none | A linked courier's slug. Without it, the courier marked is_send_default, or else the first linked one. |
wilaya_id | int | 0 | 0 gives a count per wilaya. 1 to 69 adds that wilaya's communes, stop desks and desk_send_allowed. A value outside 0 to 69 answers 400 bad_request. |
Request
curl 'https://api.dzbuild.app/v1/shipping/coverage?wilaya_id=16' \
-H "Authorization: Bearer $DZ_KEY"
Response 200
One commune and one desk are shown.
{
"data": {
"provider": "yalidine",
"is_send_default": true,
"knowledge_synced_at": "2026-10-05 03:12:44",
"wilayas": [
{ "wilaya_id": 16, "name": "Alger", "communes": 57, "communes_home": 57, "communes_desk": 12, "desks": 9 }
],
"wilaya_id": 16,
"desk_send_allowed": true,
"communes": [
{ "commune_id": 521, "name": "Alger Centre", "name_ar": "الجزائر الوسطى", "home": true, "desk": true }
],
"desks": [
{ "desk_id": "160101", "name": "Agence Alger Centre", "address": "Alger Centre", "phone": null, "commune_id": 521 }
]
}
}
| Field | Meaning |
|---|---|
knowledge_synced_at | When the platform last refreshed this courier's communes and desks. null when it never did. |
wilayas | Per wilaya: the number of communes, how many of them get home delivery (communes_home) and desk delivery (communes_desk), and the number of desks. With wilaya_id, only that wilaya. |
desk_send_allowed | Whether POST /v1/orders/{id}/send-to-delivery accepts a desk order to this wilaya with this courier. It is the same check. |
communes | commune_id is the id from GET /v1/wilayas/{id}/communes, or null when the courier's commune has no match, and name is then the courier's own name. home and desk say which delivery types the courier offers there. |
desks | The courier's stop desks in the wilaya. The Custom themes & storefronts guide shows how to offer them at checkout. |
A store with no courier answers 422 no_courier_linked. A provider the store has not linked answers 404 provider_not_linked.
Undo rate and settings changes
POST /v1/shipping/rates, PATCH /v1/shipping/settings and a rate sync save the values they replace, so each can be undone with POST /v1/changes/{id}/undo.
- A rate sync answers with its
change_id. The other two writes do not: find the change withGET /v1/changes?entity=shipping.ratesorGET /v1/changes?entity=shipping.settings, newest first. Listing changes needsstore:read. - The undo needs
shipping:writeand anIdempotency-Key. The undo is itself a change,undo_change_id, which you can undo in turn, except the undo of a sync, which answers422 nothing_to_restore. See Changes and undo. - Undoing a rate write deletes the rates of the wilayas that write created. Undoing a sync puts back the wilayas that had a rate before it; a wilaya the sync added keeps its new rate.
- The undo writes back the saved values even when the rates or settings changed again since, in the dashboard or through the API.
- A change undone a second time answers
409 already_undone. - An installed app's token cannot list 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": "shipping.rates",
"undo_change_id": 510
}
}
Errors
| HTTP | Code | Cause |
|---|---|---|
| 400 | bad_request | The body is not a JSON object, rates is missing or not an object, an id in the path is malformed, wilaya_id is outside 0 to 69, or Idempotency-Key is missing or malformed. |
| 400 | invalid_rates | rates is empty or has more than 69 wilayas, a rate is not an object, or an on/off switch is not a boolean. |
| 400, 422 | invalid_wilaya | 400: a rates key is not plain digits. 422: no wilaya has this id. |
| 400, 422 | invalid_price | 400: not a number. 422: negative or above 100000. |
| 400, 422 | invalid_days | 400: not a whole number. 422: outside 0 to 60. |
| 400 | nothing_to_update | PATCH /v1/shipping/settings without any of its three fields. |
| 400 | invalid_free_shipping | free_shipping is not a boolean. |
| 400, 422 | invalid_threshold | 400: not a number or null. 422: negative or above 99999999.99. |
| 400 | invalid_wilaya_mode | wilaya_mode is neither "58" nor "69". |
| 422 | wilaya_mode_69_unsupported | wilaya_mode is "69", which is set from the dashboard. |
| 400 | provider_required | provider is missing. |
| 400 | credentials_required | No api_id was sent or stored, or no api_token for a courier that takes two values. |
| 400 | invalid_credentials_format | zrexpressnew values that hold JSON braces, spaces or line breaks, start with http, or run over 128 characters. |
| 400 | api_url_required, custom_name_required | customecotrack without its address, or a link without its name. |
| 400, 422 | invalid_delivery_tier | 400: neither express nor economic. 422: economic for a courier other than guepex. |
| 422 | unsupported_provider | The slug is not in GET /v1/shipping/providers. |
| 422 | invalid_api_url, api_url_mismatch | The customecotrack address is not an https Ecotrack address, or differs from the stored one while the stored credentials are reused. |
| 422 | credentials_rejected | The courier refused the credentials. Nothing was saved. |
| 422 | rate_sync_unsupported | mdm and neardelivery have no price list. |
| 422 | provider_not_linked | Rate sync: the courier is not linked from the courier list, or is turned off. |
| 404 | provider_not_linked | Default, unlink or coverage: the store has not linked this courier. |
| 422 | store_wilaya_required | Yalidine-family rate sync before the store's wilaya is set. |
| 422 | confirmation_required, confirmation_stale | See the confirmation section above. |
| 422 | snapshot_too_large, snapshot_failed | The prior rates could not be saved, so the write was refused rather than made impossible to undo. |
| 422 | no_courier_linked | Coverage for a store with no courier. |
| 422 | shipping_not_available | A write on a store that sells digital products. |
| 422 | idempotency_key_reuse | The same Idempotency-Key with a different body. |
| 403 | forbidden | The key lacks the scope, for example "Missing scope: shipping:write", or a merchant key whose store is not on an active Enterprise plan. |
| 404 | not_found | Communes of a wilaya that does not exist. |
| 404 | store_not_found | The key's store no longer exists. |
| 429 | sync_cooldown | The same courier was synced less than 5 minutes ago. The message says how many minutes are left. |
| 429 | rate_limited, too_many_concurrent | The courier budget or the store's request limit is spent. See Rate limits. |
| 503 | sync_queue_failed | The sync could not be started and the rates did not change. Retry later. |
| 500 | server_error | Retry with the same Idempotency-Key. |