.dev
REST API

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

NameWhat it is
Carrier accountOne connected company on the store. Ids look like ca_….
shippingFeeIdThe 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.
carrierAccountIdThe account a send or quote request should use. Request body only.
Project feescourier-fees rows with no account — the unassigned card.
ParcelThe shipment record. Create it, then send it to the company.
Stop deskA 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:

SlugCompany
ecotrackEcotrack (portal token, or email login that exchanges for a token)
yalidineYalidine
guepexGuepex
zrexpressZR Express
ecomdeliveryE-com Delivery
noestNOEST
maystroMaystro
mdmexpressMDM Express
procolisProcolis
zimouZimou
feeef_expressFeeef Express
otherManual 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.

MethodPathDescription
GET/v1/stores/{storeId}/couriersCompany catalog
GET/v1/stores/{storeId}/carrier-accountsConnected accounts
POST/v1/stores/{storeId}/carrier-accountsConnect 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-loginExchange Ecotrack portal email/password for a token
POST/v1/stores/{storeId}/carrier-accounts/{accountId}/syncPull remote parcel state
GET/v1/stores/{storeId}/carrier-accounts/{accountId}/remote-parcelsParcels that exist at the company and not yet imported
POST/v1/stores/{storeId}/carrier-accounts/{accountId}/importImport those parcels
GET/v1/stores/{storeId}/carrier-accounts/{accountId}/webhook-eventsRecent 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).

MethodPathDescription
GET…/carrier-accounts/{accountId}/feesFee matrix for one account
PUT…/carrier-accounts/{accountId}/feesReplace that matrix
POST…/carrier-accounts/{accountId}/fees/importStart a tariff import job
GET…/carrier-accounts/{accountId}/fees/import/{jobId}Import job status
GET…/carrier-accounts/{accountId}/fees/resolveOne fee for a destination
POST…/carrier-accounts/fees/resolveResolve fees for many destinations
POST…/carrier-accounts/quotesQuote home vs stop-desk across accounts
GET/v1/stores/{storeId}/courier-feesProject fee cards
POST/v1/stores/{storeId}/courier-feesCreate a project fee card
GET/v1/stores/{storeId}/courier-fees/unassignedThe shared unassigned card
PUT/v1/stores/{storeId}/courier-fees/unassignedReplace 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

MethodPathDescription
GET/v1/stores/{storeId}/parcelsList
POST/v1/stores/{storeId}/parcelsCreate 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:batchBatch create or update
POST/v1/stores/{storeId}/parcels/{parcelId}/sendHand the parcel to the company
POST/v1/stores/{storeId}/parcels/{parcelId}/syncRefresh status from the company
POST/v1/stores/{storeId}/parcels/{parcelId}/cancelCancel at the company
GET/v1/stores/{storeId}/parcels/{parcelId}/eventsTracking events
GET/v1/stores/{storeId}/parcels/{parcelId}/labelOne label
POST/v1/stores/{storeId}/parcels/labelsLabels 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.

MethodPathDescription
POST/v1/stores/{storeId}/orders/{orderId}/cloud-parcels/sendSend one order
POST/v1/stores/{storeId}/orders/cloud-parcels/sendManySend up to 500; Cloud chunks them
POST/v1/stores/{storeId}/orders/{orderId}/cloud-parcels/unsendDetach 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.

CompanyCourier callRest between chunks
Ecotrack (DHD and the other white-labels)100 parcels3s
NOEST1002s
E-com Delivery1002.5s
Yalidine, Guepex502s
MDM Express, Zimou, Procolis502s
Maystro1 parcel, groups of 50 (300ms inside the group)2s
ZR Express1 parcel, groups of 50 (50ms inside the group)1s
DeliveryOS1 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).

MethodPathDescription
GET/v1/stores/{storeId}/label-settingsStore label defaults
PATCH/v1/stores/{storeId}/label-settingsUpdate defaults
GET/v1/stores/{storeId}/label-templatesSaved templates
POST/v1/stores/{storeId}/label-templatesCreate 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/webhook
  • POST /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.

EventWhenPayload
parcel.status_changedThe unified status movedid, fromStatus, toStatus, carrierStatusRaw, carrierAccountId
parcel.deliveredThe status became deliveredid, toStatus, carrierAccountId
parcel.deletedThe parcel was deleted through the APIid, 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 seq you 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 wantUse
Connect Yalidine, Ecotrack, ZR Express, and the rest as companies on one storeCarrier accounts on this page
Price an order form by communeshippingFeeId plus fee resolve, with geo search for the commune
Print a labelGET …/parcels/{id}/label or POST …/parcels/labels
The old …/carriers/{carrierCode}/send bridgeDelivery — kept for existing integrations

On this page