Аутентификация и подпись запроса
Каждый партнёрский запрос к AploDelivery подписывается HMAC-SHA256 четырьмя заголовками. Реализация сервера — internal/auth/hmac.go (Authenticator.MiddlewareWithLimit); эталонная клиентская реализация — pkg/ingressclient (функция signature).
Заголовки
| Заголовок | Значение |
|---|---|
X-API-Key | выданный вам API-ключ (≥ 8 символов) |
X-Timestamp | Unix time в секундах, время отправки запроса |
X-Request-ID | новый UUID (формат xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, 36 символов) на каждый запрос |
X-Signature | hex-кодированный HMAC-SHA256 от canonical-строки, ключ — ваш HMAC-секрет (≥ 32 символов) |
Canonical-строка (base string)
timestamp + "\n" + requestId + "\n" + HTTP_METHOD + "\n" + escapedPath + "\n" + sha256hex(body)
Построчно:
timestamp— то же значение, что в заголовкеX-Timestamp, как строка (не число).requestId— то же значение, что в заголовкеX-Request-ID, как есть (в нижнем или как отправлено — сервер сравнивает подпись побайтово с тем, что вы прислали вX-Signature, а самrequestIdв canonical-строке подставляется без изменения регистра).HTTP_METHOD— заглавными буквами,GETилиPOST.escapedPath— путь без query-строки, экранированный так же, как(*url.URL).EscapedPath()в Go (для путей ASCII без спецсимволов это обычный путь как есть, например/v1/pointsили/v1/orders/3fae2b7e-…). Query-параметры не входят в подпись, даже если они есть в запросе.sha256hex(body)— hex SHA-256 тела запроса. Для GET тело всегда пустое, поэтому это hex SHA-256 пустой последовательности байт (e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855— да, это действительно так выглядит SHA-256 от"", проверьте сами:printf '' | sha256sum). Для POST — SHA-256 ровно тех байт тела, которые вы отправляете; лишний пробел или другой порядок ключей JSON даст другую подпись, хотя сам JSON и будет валиден.
Строки соединяются символом \n (LF, 0x0A), без завершающего перевода строки.
GET с query-параметрами — пример
Для GET /v1/orders?externalOrderId=web-42 в подпись идёт только /v1/orders; query-параметр externalOrderId=web-42 добавляется в URL запроса, но не участвует в canonical-строке.
Допуски и одноразовость
- Расхождение часов (
clock skew) — не более 60 секунд. Сервер сравниваетX-Timestampс собственным временем; запрос старше или «из будущего» больше чем на 60 c получает401 unauthorized. Синхронизируйте время backend по NTP. X-Request-IDодноразовый. Сервер сохраняет пару «API-ключ + requestId» (INSERT ... ON CONFLICT DO NOTHINGвdelivery_auth_nonces) и отклоняет повторное использование той же пары401 unauthorized, даже если остальные поля (подпись, timestamp) корректны. Генерируйте новый UUID на каждый HTTP-запрос, включая повторные попытки (retry) — идемпотентность заказа обеспечивается отдельно, полемexternalOrderIdв теле, а неX-Request-ID(см. Идемпотентность).- Сервер не следует HTTP-редиректам — ни в своих исходящих вызовах (вебхуки), ни клиент
pkg/ingressclientв своих запросах к AploDelivery.
Тестовый вектор для проверки собственной реализации
Значения ниже — фиктивные данные, специально придуманные для проверки формулы подписи. Не используйте этот секрет ни для чего, кроме сверки своего кода.
| Параметр | Значение |
|---|---|
Секрет (APLO_API_SECRET) | test-secret-0123456789abcdef0123456789 |
X-Timestamp | 1758610800 |
X-Request-ID | 11111111-1111-4111-8111-111111111111 |
| Метод | POST |
| Путь (без query) | /v1/orders |
| Тело запроса (ровно эти байты, без добавленного перевода строки в конце) | см. ниже |
Тело запроса (валидный CanonicalOrder):
{"schemaVersion":1,"source":"website","externalOrderId":"site-1042","externalPointId":"3","createdAt":"2026-09-23T10:15:00+03:00","deliverAt":"2026-09-23T11:00:00+03:00","guest":{"externalClientId":"c-9001","name":"Ivan","phone":"+79261234567"},"address":{"street":"Lenina","house":"5"},"items":[{"externalProductId":"321","quantity":"2","price":{"amountMinor":42000,"currency":"RUB"}}],"total":{"amountMinor":84000,"currency":"RUB"}}
Промежуточные и итоговое значения:
| Величина | Значение |
|---|---|
sha256hex(body) | fa8462ece95103e1d8e7771d8bbe5b803ffda8881429cff7978b07289c2ff15a |
| Canonical-строка | 1758610800\n11111111-1111-4111-8111-111111111111\nPOST\n/v1/orders\nfa8462ece95103e1d8e7771d8bbe5b803ffda8881429cff7978b07289c2ff15a |
X-Signature (hex HMAC-SHA256) | f341b5e12580ecf285360538a21c41a663d5a28c1e7dc8f817270861614b68dc |
Значение подписи проверено двумя независимыми способами — воспроизведите любой из них и сверьте результат:
Python:
import hashlib, hmac
secret = b"test-secret-0123456789abcdef0123456789"
timestamp = "1758610800"
request_id = "11111111-1111-4111-8111-111111111111"
method = "POST"
path = "/v1/orders"
body = b'{"schemaVersion":1,"source":"website","externalOrderId":"site-1042","externalPointId":"3","createdAt":"2026-09-23T10:15:00+03:00","deliverAt":"2026-09-23T11:00:00+03:00","guest":{"externalClientId":"c-9001","name":"Ivan","phone":"+79261234567"},"address":{"street":"Lenina","house":"5"},"items":[{"externalProductId":"321","quantity":"2","price":{"amountMinor":42000,"currency":"RUB"}}],"total":{"amountMinor":84000,"currency":"RUB"}}'
body_hash = hashlib.sha256(body).hexdigest()
canonical = f"{timestamp}\n{request_id}\n{method}\n{path}\n{body_hash}"
signature = hmac.new(secret, canonical.encode(), hashlib.sha256).hexdigest()
print(signature) # f341b5e12580ecf285360538a21c41a663d5a28c1e7dc8f817270861614b68dc
curl + openssl (shell):
BODY='{"schemaVersion":1,"source":"website","externalOrderId":"site-1042","externalPointId":"3","createdAt":"2026-09-23T10:15:00+03:00","deliverAt":"2026-09-23T11:00:00+03:00","guest":{"externalClientId":"c-9001","name":"Ivan","phone":"+79261234567"},"address":{"street":"Lenina","house":"5"},"items":[{"externalProductId":"321","quantity":"2","price":{"amountMinor":42000,"currency":"RUB"}}],"total":{"amountMinor":84000,"currency":"RUB"}}'
BODY_HASH="$(printf '%s' "$BODY" | sha256sum | awk '{print $1}')"
TIMESTAMP='1758610800'
REQUEST_ID='11111111-1111-4111-8111-111111111111'
CANONICAL="$(printf '%s\n%s\n%s\n%s\n%s' "$TIMESTAMP" "$REQUEST_ID" "POST" "/v1/orders" "$BODY_HASH")"
SIGNATURE="$(printf '%s' "$CANONICAL" | openssl dgst -sha256 -hmac 'test-secret-0123456789abcdef0123456789' -hex | sed 's/^.* //')"
echo "$SIGNATURE" # f341b5e12580ecf285360538a21c41a663d5a28c1e7dc8f817270861614b68dc
Если ваша реализация даёт другое значение — самая частая причина: тело сериализовано с другим порядком ключей/пробелами, чем байты выше, либо \n заменён на \r\n, либо путь содержит query-строку.
Второй вектор — GET без тела
Для проверки ветки с пустым телом (пригодится для GET /v1/points, GET /v1/orders/{id}):
| Параметр | Значение |
|---|---|
| Секрет | test-secret-0123456789abcdef0123456789 |
X-Timestamp | 1758610800 |
X-Request-ID | 22222222-2222-4222-8222-222222222222 |
| Метод | GET |
| Путь | /v1/points |
sha256hex("") | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 |
X-Signature | 7f0aba7fa0f278a2dae872fe05324ccd74475d0e6f0c61755e7c74ef9fa92146 |
Готовые реализации
- Примеры формирования подписи приведены в Быстром старте. Контракт запросов и ответов доступен в OpenAPI.
- Другой язык — реализуйте формулу выше; сверьтесь с тестовым вектором до первого запроса к боевому окружению.
Ошибки аутентификации
| HTTP | error | Причина |
|---|---|---|
401 | unauthorized | неизвестный X-API-Key, отсутствует/невалиден один из заголовков, X-Timestamp вне допуска 60 c, X-Request-ID не является UUID, подпись не совпала, либо X-Request-ID уже был использован этим ключом раньше |
503 | authentication_unavailable | сервис не смог проверить одноразовость X-Request-ID (временная недоступность зависимости) — безопасно повторить запрос позже с тем же X-Request-ID, так как он ещё не был принят |
Подробный справочник по всем кодам ошибок — Справочник ошибок.