API versions and safe retries (idempotency)
The version is only the path prefix (/api/v1, /api/company/v1). There is no Idempotency-Key support; safe retries rely on the parcel reference field.
Last updated 22 October 2026 · Last verified 22 October 2026
Purpose#
This page explains how the developer APIs are versioned and what happens when you repeat a request. In short, the version is the path prefix, there is no Idempotency-Key support, and safe retries rely on the parcel reference you send.
Versions that exist#
| API | Path prefix | Key |
|---|---|---|
| Merchant API | /api/v1 |
sk_… |
| Company API | /api/company/v1 |
ck_… |
- The version is only the path. The API reads no version header, has no
v2route and sends no deprecation or sunset headers. - The merchant documentation page in the panel says new response fields can appear without notice, so ignore unknown fields, and that a compatibility-breaking change would come as a new version it calls
v2. Nov2exists and no dates are promised. /api/v10belongs to the platform's mobile apps. It is a separate numbering, not a newerv1, and is not documented for integrations.- Older unversioned endpoints under
/apiserve store plugins. They are not part of these versioned APIs. - The
2.0in theUser-Agentof webhook requests is not an API version.
Idempotency: what is and is not supported#
The API does not read an Idempotency-Key header or any similar header, so sending one has no effect. What exists is the reference field of POST /parcels, your own unique order number.
Merchant API. Before creating a parcel, the server looks for a parcel of your account with the same reference, whatever channel created it. If one is found, it returns HTTP 200 with "duplicate": true and the existing parcel, and charges nothing. A new parcel returns HTTP 201. Parcels whose finance status is refunded or expired are ignored, so such a reference can be reused. Parcels that have no finance status recorded at all (for example some older ones) are not matched either. The second request's other fields are not compared: the existing parcel comes back unchanged.
Company API. The same reference for the same merchant returns HTTP 409 with duplicate_reference, whatever the status of the earlier parcel. Treat it as "already created" and call GET /parcels/{reference}. That lookup covers all merchants of your company and returns the newest parcel that carries that reference, so if merchants of your company can share a reference value, list with a filter instead: GET /parcels?reference=…&merchant_id=….
Without a reference, every call creates a new parcel. On the merchant API each new parcel is also charged as usual. On the company API the merchant's balance at the company is charged only if the company set that merchant to pay From their balance in advance; by default nothing is charged when the parcel is created.
| Request | Repeating it |
|---|---|
Any GET, and POST /quote |
Safe: reads or calculates only |
POST /parcels with reference |
Safe, as above |
POST /parcels/{tracking}/cancel |
Safe after a successful cancel: a parcel that is still pending or received at the warehouse, with no external tracking number, is deleted and refunded, so the second call gives 404 not_found. Any other parcel, for example one assigned for pickup, at a hub, out for delivery, delivered, returned or sent to a carrier, gets 409 not_cancellable on every call |
POST /webhooks |
Not safe: adds another endpoint until limit_reached |
DELETE /webhooks/{id} |
Second call gives 404 not_found |
Company POST …/assign to the same courier |
Safe only while the parcel is still in "Delivery Man Assign" or "Delivery Re-Schedule" with that same courier: returned unchanged. In any other status a repeat is not a no-op, so check the parcel with GET first |
Company POST …/status |
Can fail with 409 invalid_transition, for example deliver on a delivered parcel |
Retry safely#
Give every parcel a stable reference
Use your order number and never generate a new one for a retry (up to 191 characters).
Treat 201 and 200 as success
For the merchant API both mean the parcel exists. For the company API, treat
409duplicate_referencethe same way.Retry only when the outcome is unknown
Network errors, timeouts,
429(plan-limit answers carryretry_afterseconds and aRetry-Afterheader) and500server_error. Resend the same body, one request at a time. The check is a lookup before creation and the database index on merchant and reference is not unique, so simultaneous identical requests may both create a parcel.Do not loop on other errors
422validation_failedand402insufficient_balancecreate nothing. Fix the request or top up the wallet, then resend with the same reference.
import time
import requests
URL = "https://YOUR_DOMAIN/api/v1/parcels"
HEADERS = {"Authorization": "Bearer YOUR_API_KEY", "Accept": "application/json"}
def create_parcel(payload, tries=5):
for attempt in range(tries): # payload contains a stable "reference"
try:
r = requests.post(URL, json=payload, headers=HEADERS, timeout=30)
except requests.RequestException:
time.sleep(2 ** attempt)
continue
if r.status_code in (200, 201): # 201 created, 200 already existed
return r.json()["data"]
if r.status_code == 429 or r.status_code >= 500:
time.sleep(int(r.headers.get("Retry-After", 2 ** attempt)))
continue
r.raise_for_status() # other 4xx: fix the request, do not loop
raise RuntimeError("Check GET /parcels/<reference> before retrying")
Webhooks: deduplicate on your side#
A message can be delivered more than once. A delivery counts as successful only with a 2xx answer within 10 seconds; otherwise it is retried, up to 8 attempts in all (no automatic retry after the eighth), and an event can also be resent manually. X-DS-Event-Id is identical on every attempt and manual resend of an event, X-DS-Attempt counts from 1, and X-DS-Delivery identifies one message to one endpoint. Store the event ids you handled. Order is not guaranteed, so rely on data.updated_at and status_code, or fetch GET /parcels/{tracking}.
Common mistakes#
- A new reference on every retry. Each one creates a new parcel, and on the merchant API charges it too.
- Treating
"duplicate": trueas an error. It is a success that returns the existing parcel. - Using
referenceto cancel. Cancel by tracking number: a reference resolves to the newest parcel that carries it. - Waiting for balance. With
wait_for_balancethe parcel stays asawaiting_funds; a retry returns it withduplicate: true. It expires after 24 hours without funds, and then the reference is free again.
Limits and permissions#
- The merchant API needs a platform merchant account that does not belong to a delivery company; when merchant plans are in use on the platform, the merchant also needs an active subscription to a plan that includes the API feature. The company API needs the company's key; for companies that need a subscription, an active subscription whose plan includes the API feature is also required (some internal companies, such as the platform's own, are exempt). See the overview for keys and rate limits.
- A
referenceis checked per merchant account, not across the platform.
API
POST /api/v1/parcelsGET /api/v1/parcels/{tracking}POST /api/v1/parcels/{tracking}/cancelPOST /api/company/v1/parcelsPOST /api/company/v1/parcels/{tracking}/assignPOST /api/company/v1/parcels/{tracking}/status