Идемпотентность
В AploDelivery есть два независимых механизма, которые легко перепутать: одноразовость запроса (X-Request-ID) и идемпотентность заказа (externalOrderId). Они решают разные задачи.
X-Request-ID — одноразовость HTTP-запроса
Каждый подписанный запрос должен нести новый X-Request-ID (UUID). Сервер один раз принимает конкретную пару «API-ключ + requestId» и навсегда отклоняет её повторное использование (401 unauthorized) — подробности в Аутентификации. Это защита от replay-атаки на уровне транспорта, а не механизм идемпотентности бизнес-операции: если вы повторяете один и тот же POST /v1/orders из-за таймаута, сгенерируйте новый X-Request-ID для повторной попытки — старый уже использован и будет отклонён.
externalOrderId — идемпотентность создания заказа
POST /v1/orders идемпотентен по ключу (tenantId, source, externalOrderId) плюс SHA-256 нормализованного канонического содержимого заказа (payloadSha256 в ответе). Практическое правило:
- Присвойте заказу стабильный
externalOrderIdна своей стороне до первой отправки (например ID заказа в вашей БД) и используйте его во всех повторных попытках. - Отправляете тот же
externalOrderIdс тем же содержимым (включаяcreatedAt— оно должно быть стабильным между попытками, а неtime.Now()на каждый retry) →200 OKс текущим состоянием уже созданного заказа. Дубликат не создаётся. - Отправляете тот же
externalOrderId, но содержимое отличается (другая сумма, другой адрес, другой состав) →409с{"error":"idempotency_conflict"}. Это сигнал ошибки в вашей логике повторов — не ретраить автоматически, разобраться, какое содержимое верное. - Отправляете новый
externalOrderId→ обычное создание,201 Created.
Область действия идемпотентности — конкретный credential (tenantId + source, то есть в терминах партнёра — ваш API-ключ). Один и тот же externalOrderId у разных партнёров (разных source) не конфликтует.
Что считается «тем же содержимым»
Сервер сравнивает не байты JSON, а SHA-256 от нормализованного типизированного представления заказа (payloadSha256). Практически: одинаковые значения полей дают одинаковый хэш независимо от порядка ключей или пробелов в вашем JSON — но любое отличие значения (даже отличающийся comment) даёт другой хэш и, соответственно, 409 при повторной отправке под тем же externalOrderId.
Практический паттерн ретрая
1. Сгенерировать externalOrderId один раз, сохранить его у себя ДО отправки.
2. Собрать CanonicalOrder с фиксированным createdAt.
3. POST /v1/orders с новым X-Request-ID.
4. Сетевая ошибка / таймаут / 5xx → повторить шаг 3 с ТЕМ ЖЕ externalOrderId,
ТЕМ ЖЕ телом, но НОВЫМ X-Request-ID.
5. 200 или 201 — успех, дальше работать с вернувшимся id/state.
6. 409 idempotency_conflict — остановиться, это не сетевая ошибка.
Отмена и другие операции
POST /v1/orders/{id}/cancel— идемпотентна по самому заказу: повторный вызов cancel для уже отменённого/финального заказа возвращает200(no-op) с текущим телом заказа, а не ошибку. Если предыдущая попытка отмены уже была отклонена плагином —409 cancel_rejected(это не идемпотентный успех, а терминальный отказ, см. Отмену заказа).- Остальные операции чтения (
GET /v1/orders,GET /v1/orders/{id},GET /v1/points,GET /v1/points/{pointId}/products,POST /v1/addresses/check) не имеют побочных эффектов — их можно повторять свободно, ограничение только наX-Request-IDper запрос. - Вебхуки идемпотентны на вашей стороне по
eventId— доставка at-least-once, дедупликация обязательна на вашей стороне. Подробнее — Вебхуки.