Build on the Trackboria e-commerce API.
Create delivery orders from your own system, read their status and tracking timeline, 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. Creating an order needs orders:write, reading one needs orders:read, and the tracking endpoint needs tracking:read. 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.
Order endpoints
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 needs a plan that includes it.
{
"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, and the shipment with its public event timeline. 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"
}
}
]
}
}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 your current plan does not include cross-border delivery. details names the plans that do. |
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.
