# 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](https://dzbuild.com/api-docs/guides/custom-storefronts.md).

## 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 answers `403 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_available` there.
* Every write needs an `Idempotency-Key` header. See [Idempotency](https://dzbuild.com/api-docs/idempotency.md).
* 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-delivery` share a per-store courier budget on top of the [rate limits](https://dzbuild.com/api-docs/rate-limits.md).
* 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 `GET` sent 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`. `guepex` also takes `economic`.
* `stock_account`, `ecotrack` family: fulfil orders from the courier's stock.
* `auto_validate`, `noest`: validate orders automatically at the courier.
* `api_url` and `custom_name`, `customecotrack`, both required to link: the courier's https Ecotrack address (a host ending in `.ecotrack.dz`, or `platform.dhd-dz.com` or `app.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_id` or `api_token` keeps 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_default` is `true` only when this call made the courier the default, so a default courier re-saved without `set_default` stays the default while the answer says `false`. `GET /v1/shipping/providers` shows the real state.
* `zrexpress` and `zrexpressnew` are one courier: linking one replaces the other. A `zrexpressnew` pair accepted by the older ZR Express platform is saved as `zrexpress`, and `provider` in 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.

1. Call without a confirmation. The answer is `422 confirmation_required`, and the `error` object adds `confirm_token` (single use), `confirm_token_expires_in` (`600` seconds), `action` and `will_change`, the summary to show the merchant.
2. Once the merchant approves, repeat the call with `confirm_token` in the body and a **new** `Idempotency-Key`. The first key is bound to the body without the token, so reusing it answers `422 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_price` and `desk_price` and sets `synced_provider` and `synced_at`. The on/off switches and `days` of a wilaya that already had a rate stay as they were. A wilaya that had no rate gets `3` days.
* Yalidine-family couriers need the store's wilaya, set with `wilaya_id` in `PATCH /v1/store` (see [Store](https://dzbuild.com/api-docs/resources/store.md)). Without it the call answers `422 store_wilaya_required`.
* Follow the result with `GET /v1/shipping/rates`: the rates the sync wrote name the courier in `synced_provider` and carry a new `synced_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 `202` with `status: already_running`, that sync's `sync_id` and `change_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.

* `provider` in the path is the courier's slug. Only a courier linked from the courier list can be unlinked here; any other answers `404 provider_not_linked`.
* The body carries only the confirmation: `confirm_token`, or `confirm: true` for 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: null` and `send_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](https://dzbuild.com/api-docs/guides/custom-storefronts.md) 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 with `GET /v1/changes?entity=shipping.rates` or `GET /v1/changes?entity=shipping.settings`, newest first. Listing changes needs `store:read`.
* The undo needs `shipping:write` and an `Idempotency-Key`. The undo is itself a change, `undo_change_id`, which you can undo in turn, except the undo of a sync, which answers `422 nothing_to_restore`. See [Changes and undo](https://dzbuild.com/api-docs/resources/changes.md).
* 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](https://dzbuild.com/api-docs/rate-limits.md).                                                         |
| 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`.                                                                                                                                            |
