Идемпотентность
Повторяйте безопасно — Idempotency-Key и уникальный external_id гарантируют, что таймаут не создаст два заказа.
Сеть ненадёжна: запрос может упасть по таймауту, когда заказ уже создан. В Partner API две независимые защиты от дублей.
Idempotency-Key
Передавайте уникальный заголовок Idempotency-Key в каждом создающем запросе — POST /v1/orders и
POST /v1/orders/batch:
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));
}
}
}import time, uuid, requests
def create_order(order):
order.setdefault("idempotency_key", str(uuid.uuid4())) # сохранён до первой попытки
for attempt in range(1, 6):
try:
res = requests.post(
"https://partner.sapamoto.kz/v1/orders",
headers={"Authorization": f"Bearer {KEY}", "Idempotency-Key": order["idempotency_key"]},
json=order["payload"],
timeout=10,
)
retry = res.status_code >= 500 or (res.status_code == 409 and res.json()["code"] == "idempotency_in_progress")
if not retry:
return res
except requests.RequestException:
pass
time.sleep(2 ** attempt * 0.25)
raise RuntimeError("Sapa Moto недоступен")external_id
external_id — ваш собственный номер заказа. Он уникален в пределах организации и среды — независимо от ключа
идемпотентности. Второй заказ с существующим external_id (с другого ключа, другого сервера или через неделю)
отклоняется:
{
"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 идемпотентны по смыслу: повтор
оставляет заказ в том же состоянии.