Sapa Moto

Идемпотентность

Повторяйте безопасно — Idempotency-Key и уникальный external_id гарантируют, что таймаут не создаст два заказа.

Сеть ненадёжна: запрос может упасть по таймауту, когда заказ уже создан. В Partner API две независимые защиты от дублей.

Idempotency-Key

Передавайте уникальный заголовок Idempotency-Key в каждом создающем запросе — POST /v1/orders и 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
Повтор в течение 24 часовРезультат
тот же ключ, то же теловозвращается первый ответ, байт в байт — статус, тело, тот же заказ
тот же ключ, другое тело409 idempotency_conflict — ничего не создаётся
тот же ключ, пока первый запрос ещё выполняется409 idempotency_in_progress — повторите через секунду

Ключи живут 24 часа в пределах среды API-ключа. Формат — 1–255 печатных символов ASCII; проще всего UUID v4. Создавайте ключ один раз на бизнес-операцию, сохраняйте рядом с заказом и используйте при каждом повторе этой операции.

import { randomUUID } from "node:crypto";

async function createOrder(order) {
  order.idempotencyKey ??= randomUUID(); // сохранён вместе с заказом до первой попытки
  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 или бизнес-ошибка — не повторяем
    } catch (e) {
      if (attempt === 5) throw e;
      await new Promise((r) => setTimeout(r, 2 ** attempt * 250));
    }
  }
}

external_id

external_id — ваш собственный номер заказа. Он уникален в пределах организации и среды — независимо от ключа идемпотентности. Второй заказ с существующим external_id (с другого ключа, другого сервера или через неделю) отклоняется:

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"
}

Считайте это «уже создан»: запросите order_url и продолжайте. Все методы с {id} принимают и ваш external_id.

Какие запросы повторять

ОтветПовторять?
таймаут, ошибка соединения, 500, 502, 503, 504да — с тем же Idempotency-Key, с нарастающей паузой
429 rate_limitedда — через Retry-After секунд
409 idempotency_in_progressда — через секунду
остальные 4xxнет — исправьте запрос; повтор даст тот же ответ

GET можно повторять в любой момент. PATCH, /ready, /cancel и /payment идемпотентны по смыслу: повтор оставляет заказ в том же состоянии.

Документация собирается из спецификации OpenAPI платформы. Скачать OpenAPI