Partner API
Move cargo, money and orders through Sadarak
A versioned REST API (/v1) with OpenAPI as the source of truth: read the orders assigned to you, report delivery events, and receive signed webhooks. Cash on delivery is a first-class citizen and every amount is an integer in qəpik.
What the partner API gives you
Orders, read
List and fetch the orders assigned to you with items, time window, zone, COD amount and the full status timeline. Customer name, phone and address arrive only with the orders:pii scope.
Logistics events
Report PICKED_UP, EN_ROUTE, DELIVERED and FAILED back to us. You collect a PACKED order at the hub; every event goes through the order state machine, so an impossible transition is refused instead of corrupting the order.
Webhooks
order.placed, order.status_changed, order.delivered, order.cancelled, batch.completed and order.assigned_to_partner, each signed with HMAC-SHA256 and retried with backoff for up to 12 hours.
Sandbox
A second API with its own seeded database and its own keys. A sandbox key never works against live data, and a live key never works against the sandbox.
Base URLs
https://api.turanyol.comhttp://localhost:3050Keys are bound to one environment. A sandbox key sent to the live API is refused with ERR_API_KEY_ENVIRONMENT.
Quick start
Two things to get right: send the key on every request, and verify the signature on every webhook.
1. Authenticate a request
Send your key in the X-Api-Key header. There is no OAuth dance and no bearer token — the key is the credential, so keep it server-side.
curl -sS "https://api.turanyol.com/v1/partner/orders?status=EN_ROUTE&page=1" \
-H "X-Api-Key: $ESADARAK_API_KEY" \
-H "Accept: application/json"2. Report a delivery event
Requires the logistics:write scope and an order assigned to you. Money is integer qəpik: 4750 is 47.50 ₼.
curl -sS -X POST "https://api.turanyol.com/v1/partner/orders/ES-1042/events" \
-H "X-Api-Key: $ESADARAK_API_KEY" \
-H "Content-Type: application/json" \
-d '{"type":"DELIVERED","codCollectedQepik":4750}'3. Verify a webhook signature
HMAC-SHA256 over the timestamp, a literal dot and the raw request body. Compare in constant time and reject anything older than five minutes.
import crypto from "node:crypto";
const SECRET = process.env.ESADARAK_WEBHOOK_SECRET;
const TOLERANCE_SEC = 300;
/** rawBody MUST be the exact bytes we sent (express: express.raw({ type: "application/json" })). */
export function verify(rawBody, signatureHeader) {
const parts = Object.fromEntries(
signatureHeader.split(",").map((part) => part.trim().split("=", 2)),
);
const t = Number(parts.t);
if (!Number.isInteger(t)) return false;
if (Math.abs(Date.now() / 1000 - t) > TOLERANCE_SEC) return false; // replay window
const expected = crypto
.createHmac("sha256", SECRET)
.update(`${t}.${rawBody}`)
.digest("hex");
const received = Buffer.from(parts.v1 ?? "", "hex");
const digest = Buffer.from(expected, "hex");
return received.length === digest.length && crypto.timingSafeEqual(received, digest);
}Conventions
- Every amount is an integer in qəpik (AZN minor units) and the field name ends in Qepik. Never a float.
- Errors are JSON with a stable code (ERR_*) and are never localized — map the code to your own strings.
- Product and category names are objects with an az, a ru and an en field. Pick the locale at your UI edge.
- Phone numbers are E.164 (+994XXXXXXXXX); timestamps are ISO-8601 UTC; ids are UUIDs and codes are ES-1042 / B-2081.
- List endpoints return an items array plus page, pageSize and total, and cap pageSize at 50.
Next steps
Keys are shown once at creation. Rotating one keeps the old key alive for 24 hours so you can deploy without downtime.
Up to five endpoints per key, each with its own secret and a full delivery log you can replay from.
Every ERR_* code the API can return, grouped by area.
{
"statusCode": 403,
"code": "ERR_API_KEY_SCOPE",
"message": "key is missing scope logistics:write",
"details": { "required": "logistics:write" }
}