Sapa Moto

Idempotency

Retry safely — Idempotency-Key and the unique external_id make sure a timeout never creates two orders.

Networks fail: a request can time out after the order was already created. The Partner API gives you two independent protections against duplicates.

Idempotency-Key

Send a unique Idempotency-Key header with every creating request — POST /v1/orders and POST /v1/orders/batch:

HTTP
POST /v1/orders HTTP/1.1
Authorization: Bearer sk_test_…
Idempotency-Key: 7f0c1e9a-3d2b-4c8e-9a51-2b6d0e4f8a13
Content-Type: application/json
Repeat within 24 hoursResult
same key, same bodythe first response is returned again, byte for byte — status, body, the same order
same key, another body409 idempotency_conflict — nothing is created
same key while the first request is still running409 idempotency_in_progress — retry in a second

Keys live for 24 hours per API key environment. Use 1–255 printable ASCII characters; a UUID v4 is the simplest choice. Generate the key once per business operation, store it next to your order and reuse it for every retry of that operation.

import { randomUUID } from "node:crypto";

async function createOrder(order) {
  order.idempotencyKey ??= randomUUID(); // persisted with the order before the first attempt
  for (let attempt = 1; ; attempt++) {
    try {
      const res = await fetch("https://partner.sapamoto.kz/v1/orders", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${process.env.SAPA_KEY}`,
          "Content-Type": "application/json",
          "Idempotency-Key": order.idempotencyKey,
        },
        body: JSON.stringify(order.payload),
        signal: AbortSignal.timeout(10_000),
      });
      if (res.status === 409 && (await res.clone().json()).code === "idempotency_in_progress") throw new Error("retry");
      if (res.status >= 500) throw new Error(`HTTP ${res.status}`);
      return res; // 2xx or a business error — do not retry
    } catch (e) {
      if (attempt === 5) throw e;
      await new Promise((r) => setTimeout(r, 2 ** attempt * 250));
    }
  }
}

external_id

external_id is your own order number. It is unique per organization and environment — independently of the idempotency key. A second order with an existing external_id (from another key, another server or a week later) is refused:

409 Conflict
{
  "type": "urn:sapa:problem:duplicate_external_id",
  "status": 409,
  "code": "duplicate_external_id",
  "message": "Заказ с таким внешним номером уже есть",
  "details": [],
  "order_id": "0192f1b2-6d4e-7a10-b3c2-8e1f0d9a4c57",
  "order_url": "/v1/orders/0192f1b2-6d4e-7a10-b3c2-8e1f0d9a4c57",
  "request_id": "0192f1c3-9e8d-7f6a-b5c4-d3e2f1a0b9c8"
}

Treat it as “already created”: fetch order_url and continue. Every endpoint with {id} also accepts your external_id.

Which requests to retry

ResponseRetry?
timeout, connection error, 500, 502, 503, 504yes — with the same Idempotency-Key, with backoff
429 rate_limitedyes — after Retry-After seconds
409 idempotency_in_progressyes — after a second
other 4xxno — fix the request; retrying gives the same answer

GET requests are safe to retry at any time. PATCH, /ready, /cancel and /payment are idempotent by their meaning: repeating them leaves the order in the same state.

These docs are built from the platform's OpenAPI specification. Download OpenAPI