Reference Developers

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 v2 route 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. No v2 exists and no dates are promised.
  • /api/v10 belongs to the platform's mobile apps. It is a separate numbering, not a newer v1, and is not documented for integrations.
  • Older unversioned endpoints under /api serve store plugins. They are not part of these versioned APIs.
  • The 2.0 in the User-Agent of 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#

  1. Give every parcel a stable reference

    Use your order number and never generate a new one for a retry (up to 191 characters).

  2. Treat 201 and 200 as success

    For the merchant API both mean the parcel exists. For the company API, treat 409 duplicate_reference the same way.

  3. Retry only when the outcome is unknown

    Network errors, timeouts, 429 (plan-limit answers carry retry_after seconds and a Retry-After header) and 500 server_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.

  4. Do not loop on other errors

    422 validation_failed and 402 insufficient_balance create nothing. Fix the request or top up the wallet, then resend with the same reference.

python
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": true as an error. It is a success that returns the existing parcel.
  • Using reference to cancel. Cancel by tracking number: a reference resolves to the newest parcel that carries it.
  • Waiting for balance. With wait_for_balance the parcel stays as awaiting_funds; a retry returns it with duplicate: 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 reference is checked per merchant account, not across the platform.

API

  • POST /api/v1/parcels
  • GET /api/v1/parcels/{tracking}
  • POST /api/v1/parcels/{tracking}/cancel
  • POST /api/company/v1/parcels
  • POST /api/company/v1/parcels/{tracking}/assign
  • POST /api/company/v1/parcels/{tracking}/status