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, and the tracking endpoint needs tracking:read. 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_..."{
"originCountryCode": "NG",
"corridors": [
{
"routeScope": "cross_border_intra_africa",
"destinationCountryCode": "GH",
"serviceCodes": ["standard", "express"]
},
{
"routeScope": "export_to_global",
"destinationCountryCode": "GB",
"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{
"routeScope": "cross_border_intra_africa",
"originCountryCode": "NG",
"destinationCountryCode": "GH",
"serviceCode": "standard",
"weightGrams": 2400,
"lengthCm": 30,
"widthCm": 20,
"heightCm": 12,
"declaredValueMinor": 4500000,
"hsCode": "6109"
}Amounts are integers in minor units. 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.
{
"quoteId": "ckz9q1v4t0007mnop2hj7klmn",
"state": "offered",
"routeScope": "cross_border_intra_africa",
"originCountryCode": "NG",
"destinationCountryCode": "GH",
"serviceCode": "standard",
"currency": "NGN",
"totalAmountMinor": 3875000,
"components": [
{
"kind": "freight",
"code": "base",
"label": "Freight",
"amountMinor": 2950000,
"currency": "NGN",
"certainty": "confirmed",
"bearer": "merchant"
},
{
"kind": "duty",
"code": "import_duty",
"label": "Import duty (estimate)",
"amountMinor": 675000,
"currency": "NGN",
"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.
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{
"externalOrderRef": "SHOP-1001",
"customer": {
"fullName": "Amina Bello",
"phoneE164": "+2348012345678"
},
"deliveryAddress": {
"freeTextAddress": "12 Adeola Odeku Street, Victoria Island",
"city": "Lagos",
"state": "Lagos",
"countryCode": "NG",
"landmark": "Opposite the blue bank branch"
},
"paymentType": "COD",
"codAmountMinor": 1550000,
"currency": "NGN",
"items": [
{
"sku": "TSHIRT-M-BLK",
"name": "T-shirt, medium, black",
"quantity": 2,
"unitPriceMinor": 775000
}
]
}Amounts are integers in minor units (kobo, cents). paymentType is one of COD, MobileMoney, Card, BankTransfer, or Wallet. 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.
{
"orderId": "ckz3f8p2m0001mnop4qrs5tuv",
"externalOrderRef": "SHOP-1001",
"shipmentId": "ckz3f8p2m0003mnopqw8xyz9a",
"shipmentState": "created",
"addressAssessment": {
"confidenceScore": 0.92,
"verified": true,
"needsManualVerification": false
}
}Fetch an order
GET /v1/public-api/orders/{reference}
Returns the order, its customer and items, 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_..."{
"orderId": "ckz3f8p2m0001mnop4qrs5tuv",
"externalOrderRef": "SHOP-1001",
"createdAt": "2026-08-12T09:14:03.000Z",
"cancelledAt": null,
"currency": "NGN",
"paymentType": "COD",
"codAmountMinor": 1550000,
"customer": {
"fullName": "Amina Bello",
"phoneE164": "+2348012345678"
},
"items": [
{
"sku": "TSHIRT-M-BLK",
"name": "T-shirt, medium, black",
"quantity": 2,
"unitPriceMinor": 775000
}
],
"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": "created"
},
{
"eventType": "shipment.assigned",
"occurredAt": "2026-08-12T11:02:47.000Z",
"newState": "assigned"
}
]
}
}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_..."{
"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 export hub",
"countryCode": "NG"
}
}
]
}
}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, because it is kept on the payment record and shown in the evidence document a dispute would use. 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": "GTB-7782910", "paidAt": "2026-09-10T09:12:00Z"}'{
"orderId": "ckz3f8p2m0001mnop4qrs5tuv",
"externalOrderRef": "SHOP-1001",
"payment": {
"status": "paid",
"provider": "bank_transfer",
"paidVia": "manual_bank_transfer",
"paidAt": "2026-09-10T09:12:00.000Z",
"amountMinor": 1550000,
"currency": "NGN",
"paymentLinkUrl": null,
"reference": "manual-ckz3f8p2m0001-1757495520"
}
}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_..."{
"orderId": "ckz3f8p2m0001mnop4qrs5tuv",
"externalOrderRef": "SHOP-1001",
"payment": {
"status": "pending",
"provider": "paystack",
"paidVia": null,
"paidAt": null,
"amountMinor": 1550000,
"currency": "NGN",
"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"}'{
"orderId": "ckz3f8p2m0001mnop4qrs5tuv",
"externalOrderRef": "SHOP-1001",
"cancelledAt": "2026-08-12T12:41:09.000Z",
"shipment": {
"shipmentId": "ckz3f8p2m0003mnopqw8xyz9a",
"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.
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.
| Code | Status | Meaning |
|---|---|---|
API_KEY_REQUIRED | 401 | No API key was sent. Add the Authorization or X-API-Key header. |
API_KEY_INVALID | 401 | The key was not accepted. Malformed, unknown, and revoked keys all get this same response. |
API_KEY_SCOPE_MISSING | 403 | The key is valid but missing a scope the endpoint requires. details.missingScopes lists them. |
IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_PAYLOAD | 409 | The Idempotency-Key was already used with a different request body. |
CROSS_BORDER_NOT_INCLUDED_IN_PLAN | 403 | The 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. |
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.createda shipment was created for an order.shipment.verifiedthe delivery details passed verification.shipment.assigneda rider or courier was assigned.shipment.reassignedthe shipment moved to a different rider or courier.shipment.deliveredthe parcel was delivered.shipment.failed_attempta delivery attempt failed.shipment.return_initiateda return to the merchant was started.shipment.returnedthe parcel is back with the merchant.shipment.cancelledthe shipment was cancelled.shipment.leg_starteda leg of a cross-border relay started.shipment.leg_handover_completedcustody passed between couriers at a relay handover.shipment.leg_checkpointthe shipment passed a checkpoint on a cross-border leg.
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.
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.
