Shipping
The Cloud parcel API — carrier accounts, fees, quotes, parcels, labels, tracking, and sending an order.
Shipping on Feeef is the Cloud parcel API. A store connects one or more delivery companies,
keeps a fee card per company (or a shared project card), creates parcels, and sends them.
The merchant app's delivery hub (/shipping) is this API: companies, parcels, fees, quotes,
and labels.
The older per-carrier routes under store.integrations.{carrier} still answer for stores that
were connected that way. New integrations should use the routes on this page. The legacy
bridge is documented in Delivery.
Places — commune search, stop desks, courier coverage — come from Cloud geo.
All routes below require a Bearer token and membership on the store, except the inbound Cloud parcel webhook.
Concepts
| Name | What it is |
|---|---|
| Carrier account | One connected company on the store. Ids look like ca_…. |
shippingFeeId | The store or product default fee. Either a carrier account id (ca_…) or the literal unassigned (the shared project fee card). This field is not carrierAccountId. |
carrierAccountId | The account a send or quote request should use. Request body only. |
| Project fees | courier-fees rows with no account — the unassigned card. |
| Parcel | The shipment record. Create it, then send it to the company. |
| Stop desk | A pickup point (pkp_…) from geo pickups. |
Fee resolution for an order is product shippingFeeId, then the store default, then the
cheapest connected account (in that order, the first one with a price wins). A ca_… id resolves through
POST …/carrier-accounts/fees/resolve. unassigned reads the project card and picks the
row for the customer's state.
Companies
GET /v1/stores/{storeId}/couriers lists the companies Cloud can connect. The merchant
connect wizard knows these slugs:
| Slug | Company |
|---|---|
ecotrack | Ecotrack (portal token, or email login that exchanges for a token) |
yalidine | Yalidine |
guepex | Guepex |
zrexpress | ZR Express |
ecomdelivery | E-com Delivery |
noest | NOEST |
maystro | Maystro |
mdmexpress | MDM Express |
procolis | Procolis |
zimou | Zimou |
feeef_express | Feeef Express |
other | Manual prices, no live carrier link |
Yalidine and Guepex need an origin wilaya. Ecotrack can sign in through
POST …/carrier-accounts/ecotrack-login instead of pasting a token.
| Method | Path | Description |
|---|---|---|
GET | /v1/stores/{storeId}/couriers | Company catalog |
GET | /v1/stores/{storeId}/carrier-accounts | Connected accounts |
POST | /v1/stores/{storeId}/carrier-accounts | Connect a company |
GET | /v1/stores/{storeId}/carrier-accounts/{accountId} | One account |
PUT | /v1/stores/{storeId}/carrier-accounts/{accountId} | Update credentials or config (including autoValidate) |
DELETE | /v1/stores/{storeId}/carrier-accounts/{accountId} | Disconnect |
POST | /v1/stores/{storeId}/carrier-accounts/ecotrack-login | Exchange Ecotrack portal email/password for a token |
POST | /v1/stores/{storeId}/carrier-accounts/{accountId}/sync | Pull remote parcel state |
GET | /v1/stores/{storeId}/carrier-accounts/{accountId}/remote-parcels | Parcels that exist at the company and not yet imported |
POST | /v1/stores/{storeId}/carrier-accounts/{accountId}/import | Import those parcels |
GET | /v1/stores/{storeId}/carrier-accounts/{accountId}/webhook-events | Recent inbound events for this account |
config.autoValidate tells Cloud to validate a parcel as soon as it is sent, when the
company supports it.
Fees and quotes
Each account has a wilaya (and sometimes commune) fee matrix: home delivery and stop-desk. You can edit it, or import the company's own tariff when that company supports import (Maystro, MDM, Procolis, Zimou, and Feeef Express do not).
| Method | Path | Description |
|---|---|---|
GET | …/carrier-accounts/{accountId}/fees | Fee matrix for one account |
PUT | …/carrier-accounts/{accountId}/fees | Replace that matrix |
POST | …/carrier-accounts/{accountId}/fees/import | Start a tariff import job |
GET | …/carrier-accounts/{accountId}/fees/import/{jobId} | Import job status |
GET | …/carrier-accounts/{accountId}/fees/resolve | One fee for a destination |
POST | …/carrier-accounts/fees/resolve | Resolve fees for many destinations |
POST | …/carrier-accounts/quotes | Quote home vs stop-desk across accounts |
GET | /v1/stores/{storeId}/courier-fees | Project fee cards |
POST | /v1/stores/{storeId}/courier-fees | Create a project fee card |
GET | /v1/stores/{storeId}/courier-fees/unassigned | The shared unassigned card |
PUT | /v1/stores/{storeId}/courier-fees/unassigned | Replace the shared card |
PATCH | /v1/stores/{storeId}/courier-fees/{feeId} | Update one card |
DELETE | /v1/stores/{storeId}/courier-fees/{feeId} | Delete one card |
Paths abbreviated with … start at /v1/stores/{storeId}.
The fee before ordering
POST /v1/orders/calculate (public) returns the fee an order will be charged, using the
resolution order above. Send storeId, items, shippingState, shippingType and,
optionally, shippingCity (the commune, matched before the wilaya row):
const { shippingPrice } = await ff.orders.quoteShipping({
storeId,
items: [{ productId, quantity: 1 }],
shippingState: '16',
shippingCity: 'Bab Ezzouar',
shippingType: ShippingType.home,
})
// shippingPrice: number (0 = free) or null (no price for this destination)quoteShipping ships with feeef.js 0.13. The storefront re-quotes when the wilaya, commune
or delivery type changes and keeps the static fee while a quote loads or fails.
Coming soon: a fixed custom fee on a product, and a mixed-cart split (one parcel per
courier account, each charged its highest item fee). Today a product can only point at a
fee card (shippingFeeId), and a cart whose products point at different cards is charged
one fee from the store default or the cheapest account.
Quote a destination
curl -X POST "https://api.feeef.org/v1/stores/$STORE/carrier-accounts/quotes" \
-H "Authorization: Bearer $FEEEF_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "state": "16", "city": "Bab Ezzouar", "deliveryType": "home" }'Parcels
| Method | Path | Description |
|---|---|---|
GET | /v1/stores/{storeId}/parcels | List |
POST | /v1/stores/{storeId}/parcels | Create a draft parcel |
GET | /v1/stores/{storeId}/parcels/{parcelId} | One parcel |
PATCH | /v1/stores/{storeId}/parcels/{parcelId} | Update a draft (PUT is the same handler and is deprecated) |
DELETE | /v1/stores/{storeId}/parcels/{parcelId} | Delete a draft |
POST | /v1/stores/{storeId}/parcels:batch | Batch create or update |
POST | /v1/stores/{storeId}/parcels/{parcelId}/send | Hand the parcel to the company |
POST | /v1/stores/{storeId}/parcels/{parcelId}/sync | Refresh status from the company |
POST | /v1/stores/{storeId}/parcels/{parcelId}/cancel | Cancel at the company |
GET | /v1/stores/{storeId}/parcels/{parcelId}/events | Tracking events |
GET | /v1/stores/{storeId}/parcels/{parcelId}/label | One label |
POST | /v1/stores/{storeId}/parcels/labels | Labels for many parcels |
A parcel destination is a commune from geo search. Quotes take deliveryType: home,
pickup, or store. Stop-desk sends pass pickupId. If the company does not cover the
commune, order send falls back to the nearest covered desk unless you set
coverageFallback to none (the default is nearest_covered).
Send an existing order
This builds the parcel from the order, applies an optional body patch, and sends it.
| Method | Path | Description |
|---|---|---|
POST | /v1/stores/{storeId}/orders/{orderId}/cloud-parcels/send | Send one order |
POST | /v1/stores/{storeId}/orders/cloud-parcels/sendMany | Send up to 500; Cloud chunks them |
POST | /v1/stores/{storeId}/orders/{orderId}/cloud-parcels/unsend | Detach and cancel the Cloud parcel |
curl -X POST "https://api.feeef.org/v1/stores/$STORE/orders/$ORDER/cloud-parcels/send" \
-H "Authorization: Bearer $FEEEF_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "carrierAccountId": "ca_…" }'Sending a large selection
sendMany is one Feeef request. The body is items (or orderIds) plus carrierAccountId. The merchant app can select up to 500 orders. Cloud accepts that as one batch (the batch cap is 1,000 items) and does not post 500 parcels in a single courier call.
Courier create APIs accept about 50 or 100 parcels. Cloud splits the selection into those chunks. Between chunks it waits 1–5 seconds so the next call does not trip the company's rate limit. Companies that create one parcel per request still do that, paced inside a group of 50, and take the same rest between groups.
A full selection can take several minutes. The HTTP call stays open until Cloud finishes. Allow up to 15 minutes on the client (receiveTimeout on the Dart shipOrders call). If the wait expires, the response says the batch is still processing — do not resubmit the same orders.
| Company | Courier call | Rest between chunks |
|---|---|---|
| Ecotrack (DHD and the other white-labels) | 100 parcels | 3s |
| NOEST | 100 | 2s |
| E-com Delivery | 100 | 2.5s |
| Yalidine, Guepex | 50 | 2s |
| MDM Express, Zimou, Procolis | 50 | 2s |
| Maystro | 1 parcel, groups of 50 (300ms inside the group) | 2s |
| ZR Express | 1 parcel, groups of 50 (50ms inside the group) | 1s |
| DeliveryOS | 1 parcel, groups of 50 (500ms inside the group) | 4s |
The same split applies when you send drafts that already exist: POST …/parcels:batch with operation send. How this feels from the orders desk is Sending 500 orders.
Labels
Labels can be the company's own PDF or a Feeef template (store branding, layout).
| Method | Path | Description |
|---|---|---|
GET | /v1/stores/{storeId}/label-settings | Store label defaults |
PATCH | /v1/stores/{storeId}/label-settings | Update defaults |
GET | /v1/stores/{storeId}/label-templates | Saved templates |
POST | /v1/stores/{storeId}/label-templates | Create a template |
PATCH | /v1/stores/{storeId}/label-templates/{templateId} | Update a template |
Inbound tracking
Cloud calls Feeef when a parcel moves. Both paths hit the same handler and authenticate with HMAC, not a user session:
POST /v1/integrations/cloud/parcels/webhookPOST /v1/cloud/parcels/webhook
Read the stored events with GET …/parcels/{parcelId}/events or the account log
GET …/carrier-accounts/{accountId}/webhook-events.
Parcel events
Outbound webhooks for integrators subscribed to a project's parcel events. Each delivery is
a POST with a JSON body:
{
"id": "evt_…",
"type": "parcel.status_changed",
"apiVersion": "…",
"createdAt": "…",
"payload": { "id": "…", "fromStatus": "…", "toStatus": "…", "seq": 7, "occurredAt": "…" }
}Headers: X-Feeef-Cloud-Id (the event id), X-Feeef-Cloud-Type,
X-Feeef-Cloud-Timestamp, X-Feeef-Cloud-Delivery and X-Feeef-Cloud-Signature
(t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>"> with your endpoint secret).
Verify the signature on the raw body and reject old timestamps.
| Event | When | Payload |
|---|---|---|
parcel.status_changed | The unified status moved | id, fromStatus, toStatus, carrierStatusRaw, carrierAccountId |
parcel.delivered | The status became delivered | id, toStatus, carrierAccountId |
parcel.deleted | The parcel was deleted through the API | id, carrierAccountId, reference, trackingId, status |
Every parcel event payload also has occurredAt and a per-parcel seq that increases with
each event for that parcel.
Delivery is at least once. Events are queued before they are sent and retried on any non-2xx answer (after 1 min, 5 min, 30 min, 2 h and 12 h, then dropped). So:
- dedupe on the event
id(X-Feeef-Cloud-Id); - for each parcel, keep the highest
seqyou applied and drop events with a lower one; - answer 2xx quickly and do the work afterwards.
Coming soon: parcel.deleted for parcels deleted on the courier's side (with
reason: "courier_deleted") and parcel.cancelled when the courier cancels. Today a
courier-side cancel arrives as parcel.status_changed with toStatus: "cancelled".
What to use
| You want | Use |
|---|---|
| Connect Yalidine, Ecotrack, ZR Express, and the rest as companies on one store | Carrier accounts on this page |
| Price an order form by commune | shippingFeeId plus fee resolve, with geo search for the commune |
| Print a label | GET …/parcels/{id}/label or POST …/parcels/labels |
The old …/carriers/{carrierCode}/send bridge | Delivery — kept for existing integrations |