Trackboria
Delivery Control
developer documentation

Build on the Trackboria e-commerce API.

Price a delivery, create orders from your own system, read their status and tracking timeline, cancel what is no longer wanted, and receive signed webhooks as shipments move. This page and the OpenAPI document below cover everything a third-party integration needs.

The base URL is https://api.trackboria.com. Every endpoint on this page is versioned under /v1.

Authentication

Every request needs a merchant API key. Create, rotate, and revoke keys on the Integrations screen of your merchant dashboard. A key starts with tbk_live_ and its full value is shown once, at creation. Store it in your secret manager; if you lose it, create a new key.

Send the key with each request, either as Authorization: Bearer tbk_live_... or in the X-API-Key header.

Keys carry scopes. Pricing a delivery or creating an order needs orders:write, reading one needs orders:read, cancelling one needs orders:cancel, the tracking endpoint needs tracking:read, and asking for delivery options at checkout needs checkout:rates, which allows nothing else. Cancelling is its own scope because it cannot be undone. A key that is missing a scope an endpoint requires is refused with API_KEY_SCOPE_MISSING.

The public API accepts 120 requests per minute per key. Above that, requests return HTTP 429 until the minute window passes.

Pricing and order endpoints

List corridors

GET /v1/public-api/corridors

Returns the cross-border corridors your workspace can be quoted on and the service codes priced on each, so you never have to hard-code a serviceCode. Needs the orders:read scope.

curl https://api.trackboria.com/v1/public-api/corridors   -H "Authorization: Bearer tbk_live_..."
Response, 200 OK
{
  "originCountryCode": "SN",
  "corridors": [
    {
      "routeScope": "cross_border_intra_africa",
      "destinationCountryCode": "CI",
      "serviceCodes": ["standard", "express"]
    },
    {
      "routeScope": "export_to_global",
      "destinationCountryCode": "FR",
      "serviceCodes": ["standard"]
    }
  ]
}

Price a delivery

POST /v1/public-api/quotes

Returns what a delivery would cost before you create it, itemised, so a checkout can show a real shipping price instead of a flat guess. This is the same price the merchant dashboard quotes. Needs the orders:write scope: a key that may create an order may price one.

curl -X POST https://api.trackboria.com/v1/public-api/quotes   -H "Authorization: Bearer tbk_live_..."   -H "Content-Type: application/json"   -d @quote.json
Request body
{
  "routeScope": "cross_border_intra_africa",
  "originCountryCode": "SN",
  "destinationCountryCode": "CI",
  "serviceCode": "standard",
  "weightGrams": 2400,
  "lengthCm": 30,
  "widthCm": 20,
  "heightCm": 12,
  "declaredValueMinor": 2500000,
  "hsCode": "6109"
}

Amounts are integers in minor units. Every currency is sent with two decimal places, including those with no coins: FCFA 15,000, UGX 15,000 or RWF 15,000 is sent as 1500000. weightGrams is required. Send pieces where a route is priced per parcel and distanceMetres where it is priced by distance; a route that needs one refuses without it rather than guessing. presentmentCurrency quotes you in a currency of your choice and defaults to the corridor's own.

Response, 201 Created
{
  "quoteId": "ckz9q1v4t0007mnop2hj7klmn",
  "state": "offered",
  "routeScope": "cross_border_intra_africa",
  "originCountryCode": "SN",
  "destinationCountryCode": "CI",
  "serviceCode": "standard",
  "currency": "XOF",
  "totalAmountMinor": 1450000,
  "components": [
    {
      "kind": "freight",
      "code": "base",
      "label": "Freight",
      "amountMinor": 1100000,
      "currency": "XOF",
      "certainty": "confirmed",
      "bearer": "merchant"
    },
    {
      "kind": "duty",
      "code": "import_duty",
      "label": "Import duty (estimate)",
      "amountMinor": 350000,
      "currency": "XOF",
      "certainty": "estimated",
      "bearer": "recipient"
    }
  ],
  "transitDaysMin": 4,
  "transitDaysMax": 7,
  "dutyEstimate": {
    "available": true,
    "basis": "declared_value",
    "source": "published_tariff"
  },
  "expiresAt": "2026-09-09T11:20:00.000Z"
}

A quote is what the delivery costs on today's published rate. It is not a locked price: creating an order does not reference a quote, and the amount charged is calculated when the shipment settles, from the same rate. If the published rate changes in between, the charge follows the rate rather than the quote. Quote close to the point of sale, and treat expiresAt as how long we expect the number to still be right.

Duty and tax lines are always returned with "certainty": "estimated". They are estimates. The final amount is set by the customs authority, not by Trackboria. dutyEstimate.available is false where no rate is held for that destination, which is not the same as no duty being payable, and a checkout should not present it as zero.

Delivery options at checkout

POST /v1/public-api/checkout/rates

Returns the delivery options to show a buyer at your store's checkout, at the prices you set in the dashboard under Integrations, Delivery at checkout. Your store keeps its own checkout and payment. An empty list is an answer, not an error: your checkout shows your other shipping methods. Needs the checkout:rates scope, which allows nothing else, so the key can live on your store's server.

curl -X POST https://api.trackboria.com/v1/public-api/checkout/rates \
  -H "Authorization: Bearer tbk_live_..." \
  -H "Content-Type: application/json" \
  -d @cart.json
Request body
{
  "destination": {
    "countryCode": "NG",
    "region": "LA",
    "regionName": "Lagos",
    "city": "Ikeja"
  },
  "currency": "NGN",
  "goodsTotalMinor": 1500000,
  "items": [
    { "sku": "TSHIRT-M", "quantity": 2, "unitPriceMinor": 750000, "weightGrams": 250 }
  ],
  "locale": "en"
}

region may be a name or the code your store uses; send regionName as well when you have it. Amounts are in minor units (hundredths of the currency). Show the buyer name and description, and put code on the order's shipping line.

Response, 200 OK
{
  "rates": [
    {
      "service": "standard",
      "code": "TRACKBORIA_STANDARD",
      "name": "Standard delivery",
      "description": "Arrives today or tomorrow",
      "priceMinor": 150000,
      "currency": "NGN",
      "free": false,
      "minDays": 0,
      "maxDays": 1,
      "earliestDate": "2026-09-29",
      "latestDate": "2026-09-30"
    }
  ],
  "reason": null
}

Create an order

POST /v1/public-api/orders

Creates an order and its shipment. Needs the orders:write scope. externalOrderRef is your own order reference; you can use it later to fetch the order.

curl -X POST https://api.trackboria.com/v1/public-api/orders \
  -H "Authorization: Bearer tbk_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: SHOP-1001-attempt-1" \
  -d @order.json
Request body
{
  "externalOrderRef": "SHOP-1001",
  "customerType": "B2C",
  "customer": {
    "fullName": "Kwame Mensah",
    "phoneE164": "+233241234567"
  },
  "deliveryAddress": {
    "freeTextAddress": "14 Oxford Street, Osu",
    "city": "Accra",
    "state": "Greater Accra",
    "countryCode": "GH",
    "landmark": "Opposite the blue bank branch",
    "accessNotes": "Call on arrival, gate is locked"
  },
  "paymentType": "COD",
  "codAmountMinor": 45000,
  "currency": "GHS",
  "items": [
    {
      "sku": "TSHIRT-M-BLK",
      "name": "T-shirt, medium, black",
      "quantity": 2,
      "unitPriceMinor": 22500
    }
  ]
}

Amounts are integers in minor units (kobo, cents). Every currency is sent with two decimal places, including those with no coins: FCFA 15,000, UGX 15,000 or RWF 15,000 is sent as 1500000. paymentType is one of COD, MobileMoney, Card, BankTransfer, or Wallet, and codAmountMinor is what is still to be collected from the customer (see Payment below). customer.phoneE164 is the number the delivery code is sent to: send it in full, starting with + and the country code, or in local form as the customer would dial it. A number that cannot be rung is refused with CUSTOMER_PHONE_INVALID. deliveryAddress needs freeTextAddress, city and state; landmark and accessNotes are optional and are shown to the rider. deliveryAddress.countryCode is optional and defaults to your own country; a different country makes the order cross-border, which every plan includes and which carries a per-shipment service fee. customerType says who is buying: B2C for an individual or consumer, B2B for a business or company. Send it when you know it. When you leave it out, an order whose customer.taxId contains a digit is recorded as B2B and every other order as B2C, and you can change it on the order in your dashboard. A B2B order also sends customer.businessName (the legal name), customer.countryCode, and customer.taxId where the buyer has one. Whether the sale is domestic or cross-border is worked out from your country and the customer's, and returned as customer.transactionScope.

Request body, a business buyer
{
  "externalOrderRef": "SHOP-1002",
  "customerType": "B2B",
  "customer": {
    "fullName": "Achieng Otieno",
    "phoneE164": "+254712345678",
    "email": "accounts@thikatraders.example",
    "businessName": "Thika Traders Ltd",
    "taxId": "P051234567Q",
    "countryCode": "KE"
  },
  "billingAddress": {
    "freeTextAddress": "7 Kenyatta Highway",
    "city": "Thika",
    "countryCode": "KE"
  },
  "deliveryAddress": {
    "freeTextAddress": "7 Kenyatta Highway",
    "city": "Thika",
    "state": "Kiambu",
    "countryCode": "KE"
  },
  "paymentType": "BankTransfer",
  "codAmountMinor": 0,
  "currency": "KES",
  "items": [
    { "name": "Kitenge, 6 yards", "quantity": 10, "unitPriceMinor": 116000 }
  ]
}

When you send customerType, the tax number is checked against the format of the buyer's country where one is published: an official format that does not match is refused with CUSTOMER_TAX_ID_FORMAT, and a secondary one only returns customer.taxIdFormatWarning. When you leave customerType out, the order is never refused over buyer details: a business with no customer.businessName is invoiced to customer.fullName, its country is taken from billingAddress and then from the delivery address, and a tax number in an unusual format is kept and marked for you to check. billingAddress is optional and defaults to the delivery address. Your receipts and invoices are produced from these fields, with VAT shown when you have told us you are VAT-registered. Nothing about the buyer is ever guessed from a name, an email or an address.

Response, 201 Created
{
  "orderId": "ckz3f8p2m0001mnop4qrs5tuv",
  "externalOrderRef": "SHOP-1001",
  "shipmentId": "ckz3f8p2m0003mnopqw8xyz9a",
  "shipmentState": "created",
  "addressAssessment": {
    "confidenceScore": 0.92,
    "verified": true,
    "needsManualVerification": false
  },
  "customer": {
    "type": "B2C",
    "country": "GH",
    "transactionScope": "DOMESTIC",
    "taxIdStatus": "NOT_REQUIRED",
    "taxIdFormatWarning": false
  }
}

Fetch an order

GET /v1/public-api/orders/{reference}

Returns the order, its customer and items, deliveryAddress as it is stored (with landmark and accessNotes), the shipment with its public event timeline, and a payment block: the latest payment recorded, requested, or confirmed, or null when there is none. reference is either the Trackboria orderId or your own externalOrderRef. Needs the orders:read scope.

curl https://api.trackboria.com/v1/public-api/orders/SHOP-1001 \
  -H "Authorization: Bearer tbk_live_..."
Response, 200 OK
{
  "orderId": "ckz3f8p2m0001mnop4qrs5tuv",
  "externalOrderRef": "SHOP-1001",
  "createdAt": "2026-08-12T09:14:03.000Z",
  "orderPlacedAt": null,
  "cancelledAt": null,
  "currency": "GHS",
  "paymentType": "COD",
  "codAmountMinor": 45000,
  "payment": null,
  "metadata": null,
  "customer": {
    "fullName": "Kwame Mensah",
    "phoneE164": "+233241234567"
  },
  "deliveryAddress": {
    "freeTextAddress": "14 Oxford Street, Osu",
    "landmark": "Opposite the blue bank branch",
    "accessNotes": "Call on arrival, gate is locked",
    "city": "Accra",
    "stateRegion": "Greater Accra",
    "postalCode": null,
    "countryCode": "GH"
  },
  "buyer": {
    "customerType": "B2C",
    "customerCountry": "GH",
    "transactionScope": "DOMESTIC",
    "businessName": null,
    "taxId": null,
    "taxIdStatus": "NOT_REQUIRED"
  },
  "fiscal": {
    "sale": null,
    "corrections": []
  },
  "items": [
    {
      "sku": "TSHIRT-M-BLK",
      "name": "T-shirt, medium, black",
      "quantity": 2,
      "unitPriceMinor": 22500
    }
  ],
  "shipment": {
    "shipmentId": "ckz3f8p2m0003mnopqw8xyz9a",
    "shipmentState": "in_transit",
    "paymentState": "expected",
    "deliveredAt": null,
    "etaAt": "2026-08-13T16:00:00.000Z",
    "timeline": [
      {
        "eventType": "shipment.created",
        "occurredAt": "2026-08-12T09:14:03.000Z",
        "newState": null
      },
      {
        "eventType": "shipment.assigned",
        "occurredAt": "2026-08-12T11:02:47.000Z",
        "newState": "assigned"
      }
    ]
  }
}

buyer is who is buying on this order, as you sent it or as it was decided when you left it out. fiscal is for filing sales with a tax authority. Filing is not switched on in any market yet, so today fiscal.sale is null or has status NOT_REQUIRED, and there is no fiscal number, verification code or QR to print. Your receipts and invoices are issued all the same, from the order in your dashboard. Once filing is on for your market, fiscal.sale is the sale's fiscal document and fiscal.corrections its credit and debit notes. A store that prints its own invoice then prints fiscalNumber, verificationCode, qrPayload and every entry of printedMarks, each under its label, once status is ACCEPTED. status is one of NOT_REQUIRED, REQUIRED, PENDING, SUBMITTED, ACCEPTED, REJECTED, RETRY_REQUIRED, CANCELLATION_REQUESTED or CANCELLED; statusReason says why.

Fetch tracking

GET /v1/public-api/orders/{reference}/tracking

Returns shipment status and events only, without customer or item details, so it can back systems that should not see order contents. Cross-border checkpoint events carry the checkpoint name and country. reference works the same way as above. Needs the tracking:read scope.

curl https://api.trackboria.com/v1/public-api/orders/SHOP-1001/tracking \
  -H "Authorization: Bearer tbk_live_..."
Response, 200 OK
{
  "orderId": "ckz3f8p2m0001mnop4qrs5tuv",
  "externalOrderRef": "SHOP-1001",
  "shipment": {
    "shipmentId": "ckz3f8p2m0003mnopqw8xyz9a",
    "shipmentState": "in_transit",
    "paymentState": "expected",
    "deliveredAt": null,
    "etaAt": "2026-08-13T16:00:00.000Z",
    "events": [
      {
        "eventType": "shipment.leg_started",
        "occurredAt": "2026-08-12T14:20:00.000Z",
        "previousState": "assigned",
        "newState": "in_transit",
        "checkpoint": null
      },
      {
        "eventType": "shipment.leg_checkpoint",
        "occurredAt": "2026-08-13T08:05:12.000Z",
        "previousState": null,
        "newState": null,
        "checkpoint": {
          "name": "Arrived at sorting hub",
          "countryCode": "GH"
        }
      }
    ]
  }
}

Payment

Every order carries two things: paymentType, how the customer pays, and codAmountMinor, how much is still to be collected from them. Read together they describe three situations. Already paid: the store took the money at its own checkout, by transfer, or in cash before dispatch. Send codAmountMinor: 0, then record the payment below so it is on file. Collect through Trackboria: the customer has not paid yet. Send the full amount in codAmountMinor, then request a payment link below. Cash on delivery: paymentType: COD with the cash amount, and the rider collects it. Trackboria never creates a payment link on its own, because a store that already took payment must never have its customer charged twice.

Record a payment the store already took

POST /v1/public-api/orders/{reference}/mark-paid

For a customer who paid the store directly: a bank transfer, a card terminal in the shop, cash before dispatch. This is the same action as the dashboard's Mark as paid, with the same rules: it is refused if the order already has a successful payment, and refused if cash was already collected on delivery. paidVia is one of manual_bank_transfer, manual_cash or manual_other. Send your own reference, the transfer reference or receipt number: it is kept on the payment record, shown in the evidence document a dispute would use, and returned as payment.reference here and whenever you fetch the order. Be clear about what this is: a statement by the store, recorded with who said it and when. Trackboria did not see the money and cannot verify it, so the store carries responsibility for it. Needs orders:write.

curl -X POST https://api.trackboria.com/v1/public-api/orders/SHOP-1001/mark-paid \
  -H "Authorization: Bearer tbk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"paidVia": "manual_bank_transfer", "reference": "BANK-7782910", "paidAt": "2026-09-10T09:12:00Z"}'
Response, 201 Created
{
  "orderId": "ckz3f8p2m0001mnop4qrs5tuv",
  "externalOrderRef": "SHOP-1001",
  "payment": {
    "status": "paid",
    "provider": null,
    "paidVia": "manual_bank_transfer",
    "paidAt": "2026-09-10T09:12:00.000Z",
    "amountMinor": 45000,
    "currency": "GHS",
    "paymentLinkUrl": null,
    "reference": "BANK-7782910"
  }
}

Ask Trackboria to collect

POST /v1/public-api/orders/{reference}/payment-link

For a store that does not take payment itself. Returns a paymentLinkUrl to show the customer; when they pay, the order is marked paid by the provider's confirmation and settles to the merchant's payout account. Refused for COD orders, which the rider collects, and for orders already paid. Calling it again returns the existing active link rather than a second one. One thing to plan for: until the customer has paid, the order shows the amount as still to be collected, so a store using this should hold the delivery until the payment is confirmed rather than dispatch on creation. Needs orders:write.

curl -X POST https://api.trackboria.com/v1/public-api/orders/SHOP-1001/payment-link \
  -H "Authorization: Bearer tbk_live_..."
Response, 201 Created
{
  "orderId": "ckz3f8p2m0001mnop4qrs5tuv",
  "externalOrderRef": "SHOP-1001",
  "payment": {
    "status": "pending",
    "provider": "paystack",
    "paidVia": null,
    "paidAt": null,
    "amountMinor": 45000,
    "currency": "GHS",
    "paymentLinkUrl": "https://checkout.paystack.com/abc123def456",
    "reference": "TRK-ORD-ckz3f8p2m0001-1757495520"
  }
}

Cancel an order

POST /v1/public-api/orders/{reference}/cancel

Stops a delivery the store no longer wants made. reference works the same way as above, and reason is optional and stored on the order. Needs the orders:cancel scope, which is separate from orders:write on purpose: a cancellation cannot be undone, so a storefront key that creates orders does not have to carry the power to end them.

curl -X POST https://api.trackboria.com/v1/public-api/orders/SHOP-1001/cancel   -H "Authorization: Bearer tbk_live_..."   -H "Content-Type: application/json"   -d '{"reason": "Customer cancelled at checkout"}'
Response, 200 OK
{
  "orderId": "ckz3f8p2m0001mnop4qrs5tuv",
  "externalOrderRef": "SHOP-1001",
  "cancelledAt": "2026-08-12T12:41:09.000Z",
  "shipmentState": "cancelled"
}

Idempotency

Send a unique Idempotency-Key header with every create request. Retrying with the same key and the same body returns the stored result instead of creating a second order, so a timed-out request is always safe to retry.

Without the header, externalOrderRef is used as the idempotency key, so a retried request still cannot double-create. Sending the header explicitly remains the recommended path.

Reusing a key with a different body is refused with HTTP 409 and the code IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_PAYLOAD. Send a new key for a new request, or resend the original body unchanged to receive the stored response. An order cannot be changed by sending it again: change it on the order in your dashboard, or cancel it and create a new one.

Errors

Errors are JSON with a stable shape. Branch on code: it never changes. message is a sentence for a person and its wording can change between releases.

Messages are returned in English, or in French when the Accept-Language header prefers it. Codes are identical in both.

{
  "error": "Forbidden",
  "message": "This API key is missing required scopes: orders:write. Create a key that includes them in your Trackboria dashboard.",
  "statusCode": 403,
  "code": "API_KEY_SCOPE_MISSING",
  "details": { "missingScopes": ["orders:write"] }
}

Validation failures return HTTP 400 with the code VALIDATION_FAILED and a details.fields list naming each failing field as a dotted path, for example items.0.unitPriceMinor.

CodeStatusMeaning
API_KEY_REQUIRED401No API key was sent. Add the Authorization or X-API-Key header.
API_KEY_INVALID401The key was not accepted. Malformed, unknown, and revoked keys all get this same response.
API_KEY_SCOPE_MISSING403The key is valid but missing a scope the endpoint requires. details.missingScopes lists them.
IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_PAYLOAD409The Idempotency-Key was already used with a different request body.
CROSS_BORDER_NOT_INCLUDED_IN_PLAN403The delivery country differs from your own and cross-border delivery is not active on this account. Every plan includes it, so this is an account problem rather than a plan problem: contact support and we will open it.
CUSTOMER_BUSINESS_NAME_REQUIRED400An order sent as B2B needs customer.businessName, the buyer's legal name.
CUSTOMER_TYPE_B2C_HAS_BUSINESS_DETAILS400An order sent as B2C carried a business name or a tax number. Send B2B, or drop those fields.
CUSTOMER_TAX_ID_FORMAT400On an order that states customerType, the buyer's tax number does not match the official format of their country. The message names the format.
CUSTOMER_PHONE_INVALID400The customer's phone number cannot be rung. Send the full number, with the country code or in local form, because the delivery code is sent to it.

Outgoing webhooks

Register an HTTPS endpoint on the Integrations screen and choose the events it receives. Each endpoint gets its own signing secret, prefixed whsec_, shown once at creation.

Event catalogue

An endpoint can subscribe to individual events or to all of them with *. Internal operational events never leave the platform; these are the events that can be delivered:

  • shipment.created a shipment was created for an order.
  • shipment.verified the delivery details passed verification.
  • shipment.assigned a rider or courier was assigned.
  • shipment.reassigned the shipment moved to a different rider or courier.
  • shipment.picked_up The parcel was collected from you.
  • shipment.in_transit The parcel is on its way to the customer.
  • shipment.arriving The parcel is close to the customer, whoever carries it.
  • shipment.delivered the parcel was delivered.
  • shipment.failed_attempt a delivery attempt failed.
  • shipment.return_initiated a return to the merchant was started.
  • shipment.returned the parcel is back with the merchant.
  • shipment.cancelled the shipment was cancelled.
  • shipment.leg_started a stage of a cross-border shipment started.
  • shipment.leg_handover_completed the parcel changed hands between carriers on a cross-border shipment.
  • shipment.leg_checkpoint the shipment passed a checkpoint on a cross-border leg.
  • fiscal_document.accepted The tax authority accepted the sale's fiscal document; its number, verification code and QR are in the payload.
  • fiscal_document.rejected The tax authority rejected it; the payload says why.
  • fiscal_document.cancelled The fiscal document was cancelled with the authority.
  • fiscal_document.needs_attention The sale cannot reach the authority until you act.

These four events are sent once filing with a tax authority is switched on for your market, which is not yet the case in any market; subscribing to them now does no harm. The four fiscal_document events carry the fiscal document as it stands, in the same shape as fiscal.sale on the order, with orderId and externalOrderRef. needs_attention means the sale waits on you: its customer type or the buyer's tax number is missing, a credential needs renewing, or a deadline passed.

Fiscal event payload
{
  "event": "fiscal_document.accepted",
  "occurredAt": "2026-08-12T09:15:40.000Z",
  "data": {
    "orderId": "ckz3f8p2m0001mnop4qrs5tuv",
    "externalOrderRef": "SHOP-1001",
    "fiscalDocumentId": "fd_ckz3f8p2m0009",
    "documentType": "fiscal_invoice",
    "country": "GH",
    "commercialDocument": { "number": "INV-000104", "kind": "invoice" },
    "status": "ACCEPTED",
    "fiscalNumber": "GH-2026-000104",
    "authorityReference": "AUTH-7F3A9C",
    "verificationCode": "7F3A9C",
    "qrPayload": "...",
    "printedMarks": [{ "label": "EDC ID", "value": "..." }]
  }
}

Delivery format

Deliveries are POST requests with a JSON body. The top-level id matches the X-Trackboria-Delivery header. A delivery can arrive more than once; store the id and skip ones you have already processed.

POST https://example.com/webhooks/trackboria
Content-Type: application/json
User-Agent: Trackboria-Webhooks/1.0
X-Trackboria-Signature: t=1755082930,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
X-Trackboria-Event: shipment.delivered
X-Trackboria-Delivery: ckz3f8p2m0009mnopde1fgh2i

{
  "id": "ckz3f8p2m0009mnopde1fgh2i",
  "event": "shipment.delivered",
  "occurredAt": "2026-08-13T15:42:10.000Z",
  "data": {
    "orderId": "ckz3f8p2m0001mnop4qrs5tuv",
    "externalOrderRef": "SHOP-1001",
    "shipmentId": "ckz3f8p2m0003mnopqw8xyz9a",
    "shipmentState": "delivered",
    "previousState": "arriving"
  }
}

Verifying signatures

The X-Trackboria-Signature header has the form t=timestamp,v1=signature. The signature is HMAC-SHA256, hex encoded, computed with your endpoint secret over the string timestamp + "." + rawBody, where the timestamp is unix seconds and the body is the raw bytes received. Compare in constant time, and reject deliveries whose timestamp is more than five minutes from your clock.

Node.js verification
const { createHmac, timingSafeEqual } = require('node:crypto');

// rawBody must be the exact bytes received, before any JSON parsing.
function verifyTrackboriaSignature(secret, rawBody, signatureHeader) {
  const parts = Object.fromEntries(
    signatureHeader.split(',').map((part) => part.trim().split('=')),
  );
  const timestamp = Number(parts.t);
  if (!Number.isFinite(timestamp) || !parts.v1) return false;

  // Reject replays of captured deliveries.
  if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;

  const expected = createHmac('sha256', secret)
    .update(timestamp + '.' + rawBody)
    .digest('hex');
  const expectedBuffer = Buffer.from(expected, 'hex');
  const presentedBuffer = Buffer.from(parts.v1, 'hex');
  return (
    expectedBuffer.length === presentedBuffer.length &&
    timingSafeEqual(expectedBuffer, presentedBuffer)
  );
}

Retries and automatic disabling

Respond with any 2xx status within 10 seconds; anything else counts as a failed attempt. Failed deliveries are retried after 1 minute, 5 minutes, 30 minutes, 2 hours, and 12 hours. After the last retry the delivery is dead-lettered; the delivery log on the Integrations screen keeps it visible.

After 15 consecutive failed attempts an endpoint is disabled automatically and you are emailed. Any successful delivery resets the counter. Re-enable the endpoint from the Integrations screen once your receiver is healthy.

OpenAPI document

The machine-readable schema for every public API endpoint, generated from the API itself and kept current by a CI check. Import it into Postman, Insomnia, or a code generator.

Download openapi.json