Skip to content
TURANYOLDevelopers

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

Live
https://api.turanyol.com
live
Sandbox
http://localhost:3050
sandbox

Keys 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.

bash
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 ₼.

bash
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.

javascript
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

Create your first key

Keys are shown once at creation. Rotating one keeps the old key alive for 24 hours so you can deploy without downtime.

Point a webhook at your endpoint

Up to five endpoints per key, each with its own secret and a full delivery log you can replay from.

Read the error catalogue

Every ERR_* code the API can return, grouped by area.

error envelope
{
  "statusCode": 403,
  "code": "ERR_API_KEY_SCOPE",
  "message": "key is missing scope logistics:write",
  "details": { "required": "logistics:write" }
}