Автентифікація
API-токени asfa_ і підпис запитів обміну X-Signature v2: алгоритм, приклади на curl, Python і JavaScript, контрольний приклад.
В ApexSFA два способи автентифікації інтеграцій:
| Спосіб | Де застосовується | Заголовки |
|---|---|---|
API-токен asfa_… | Файли заказів виробника (/api/v1/exchange/*) | Authorization: Bearer asfa_… |
| Ключ обміну + підпис | Обмін з обліковою системою (/api/v1/webhooks/1c/*) | X-Tenant-Id, X-Signature: t=…,v2=… |
Вихідні вебхуки ApexSFA підписує секретом підписки — перевірка описана в розділі 6.
Усі запити — лише через HTTPS. Секрети (токени, ключі обміну, ключі SFTP) не передавайте в URL, не пишіть у логи й не комітьте в репозиторій.
2.1. API-токени
Формат і зберігання
- Токен має вигляд
asfa_+ 40 випадкових символів, наприкладasfa_Qm9rZ3hYc2VjcmV0LWV4YW1wbGUtMTIzNDU2Nzg5(вигаданий). - ApexSFA зберігає лише хеш токена. Відкритий токен показується один раз — під час видачі або перевипуску. Загубили — лише перевипуск.
Передача
Authorization: Bearer asfa_Qm9rZ3hYc2VjcmV0LWV4YW1wbGUtMTIzNDU2Nzg5Тенант визначається токеном — X-Tenant-Id не потрібен.
Права токена
Права токена задаються scope. Для файлів заказів потрібен scope exchange:orders — його ми вказуємо під час видачі. Токен для файлів заказів привʼязується до конкретного виробника: заявки з файлів цього токена отримують виробника з токена, навіть якщо реквізити постачальника у файлі неповні.
Видача, ротація, відкликання
Токени видає команда ApexSFA на запит дистрибʼютора. Один токен — один виробник.
- Перевипуск — новий секрет; старий перестає діяти одразу. Щоб перейти без простою, попросіть другий токен, переключіть клієнт і після цього відкличте перший.
- Відкликання — токен перестає діяти негайно.
- Рекомендована ротація: раз на 6–12 місяців і негайно — при підозрі на витік.
Ліміт запитів
На кожен токен — 100 запитів у вікні 60 с (якщо для токена не погоджено інше). Вікно починається з першого запиту. Перевищення → 429; заголовка Retry-After немає — зачекайте до хвилини.
Помилки API-токена
| HTTP | message | Причина |
|---|---|---|
| 401 | Bearer API token required | Немає заголовка Authorization: Bearer … |
| 401 | Invalid API token | Токен не знайдено (помилка в токені або його перевипущено) |
| 401 | Token is revoked | Токен відкликано |
| 401 | Token has expired | Минув строк дії токена |
| 403 | Token scope 'exchange:orders' required | Токену бракує прав |
| 429 | Rate limit exceeded: 100/min | Перевищено ліміт токена |
2.2. Підпис запитів обміну з обліковою системою
Ендпоінти обміну не використовують токени. Кожен запит підписується ключем обміну — секретом, який знають лише ApexSFA і ваша облікова система.
Заголовки
| Заголовок | Обовʼязковий | Опис |
|---|---|---|
X-Tenant-Id | так* | UUID тенанта (точки синхронізації). *Можна не передавати, якщо в JSON-тілі є "tenant_id" |
X-Signature | так | t=<unix-час>,v2=<hex> |
Content-Type | так | application/json; charset=utf-8 |
Алгоритм v2
t = поточний Unix-час у секундах (ціле, UTC)
body = ТОЧНІ байти тіла запиту, як вони підуть у мережу (UTF-8)
message = str(t) + "." + body + "." + secret
v2 = hex( SHA-256( message ) ) # 64 символи, нижній регістр
X-Signature: t=<t>,v2=<v2>Схема однаково реалізується на будь-якій платформі, де є SHA-256.
Увага. Підписуйте рівно ті байти, що відправляєте. Не форматуйте JSON повторно після підпису, не додавайте BOM, не змінюйте переводи рядків — інакше 401.
- Порожнє тіло сервер трактує як
{}— для запитів без даних відправляйте тіло{}і підписуйте його. - Розбіжність
tіз часом сервера — не більше ±300 с. Синхронізуйте годинник (NTP), інакше всі запити отримуватимуть 401. - Той самий заголовок
X-Signature(той самийtі те саме тіло) вдруге не приймається. Для повторної відправки того ж пакета підпишіть його заново з новимt. Якщо надсилаєте однакові тіла частіше ніж раз на секунду (наприклад, кількаoutbox/pullпоспіль), додайте в тіло унікальне поле"request_nonce": "<uuid>"— невідомі поля сервер ігнорує.
Приклади підпису
BODY='{"batch_size":50}'
TS=$(date +%s)
SIG=$(printf '%s' "$TS.$BODY.$SECRET" | sha256sum | cut -d' ' -f1)
curl -s -X POST "https://api.apexsfa.com/api/v1/webhooks/1c/outbox/pull" \
-H "Content-Type: application/json; charset=utf-8" \
-H "X-Tenant-Id: $TENANT_ID" \
-H "X-Signature: t=$TS,v2=$SIG" \
--data-binary "$BODY"import hashlib, json, time, requests
BASE = "https://api.apexsfa.com/api/v1"
TENANT_ID = "6a1d2c3e-0000-4000-8000-000000000001"
SECRET = "<ключ обміну>"
def sign_v2(body: bytes, secret: str, ts: int | None = None) -> str:
ts = ts or int(time.time())
digest = hashlib.sha256(f"{ts}.".encode() + body + f".{secret}".encode()).hexdigest()
return f"t={ts},v2={digest}"
body = json.dumps({"batch_size": 50}, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
resp = requests.post(
f"{BASE}/webhooks/1c/outbox/pull",
data=body, # саме ці байти, не json=…
headers={
"Content-Type": "application/json; charset=utf-8",
"X-Tenant-Id": TENANT_ID,
"X-Signature": sign_v2(body, SECRET),
},
timeout=60,
)
print(resp.status_code, resp.json())// Node.js 18+
import { createHash } from "node:crypto";
const BASE = "https://api.apexsfa.com/api/v1";
const TENANT_ID = "6a1d2c3e-0000-4000-8000-000000000001";
const SECRET = process.env.APEX_SECRET;
function signV2(body, secret, ts = Math.floor(Date.now() / 1000)) {
const digest = createHash("sha256").update(`${ts}.${body}.${secret}`, "utf8").digest("hex");
return `t=${ts},v2=${digest}`;
}
const body = JSON.stringify({ batch_size: 50 });
const resp = await fetch(`${BASE}/webhooks/1c/outbox/pull`, {
method: "POST",
headers: {
"Content-Type": "application/json; charset=utf-8",
"X-Tenant-Id": TENANT_ID,
"X-Signature": signV2(body, SECRET),
},
body, // той самий рядок, що підписано
});
console.log(resp.status, await resp.json());Псевдокод для будь-якої платформи
Функція ПідписЗапиту(ТілоРядкомUTF8, Секрет)
Час = ПоточнийUnixЧасUTC() // ціле число секунд
Рядок = Рядок(Час) + "." + ТілоРядкомUTF8 + "." + Секрет
Хеш = SHA256(БайтиUTF8(Рядок)) // без BOM
Повернути "t=" + Рядок(Час) + ",v2=" + НижнійРегістр(HEX(Хеш))
КінецьФункціїКонтрольний приклад
Значення вигадані — лише для перевірки вашої реалізації:
secret = test-secret-0123456789
t = 1761552000
body = {"batch_size":50}
рядок = 1761552000.{"batch_size":50}.test-secret-0123456789
v2 = 643a26cf3cd6ad12c10bc6c27e729aa4449e5630a515e7459e01b20c4c08ea4c
X-Signature: t=1761552000,v2=643a26cf3cd6ad12c10bc6c27e729aa4449e5630a515e7459e01b20c4c08ea4cПеревірка в терміналі: printf '%s' '1761552000.{"batch_size":50}.test-secret-0123456789' | sha256sum має дати той самий hex.
Ключ обміну й ротація
- Кожна точка синхронізації (філія з окремою базою обліку) має свій тенант і свій ключ обміну. Запит із ключем точки працює лише з даними цієї точки.
- Ключ видає команда ApexSFA під час підключення точки. Передайте його в налаштування обміну облікової системи; у коді, логах і листуванні ключ не зберігайте.
- При заміні ключа попередній діє ще 24 години — за цей час оновіть ключ у налаштуваннях обміну.
- Обмін тимчасово недоступний (401), якщо його вимкнено для тенанта або тенант призупинено.
Помилки автентифікації обміну
| HTTP | Причина |
|---|---|
401 INVALID_CREDENTIALS | Немає або невірний X-Tenant-Id; обмін не підключено або вимкнено; тенант призупинено; невірний підпис; час поза ±300 с; повтор підпису |
429 RATE_LIMIT_EXCEEDED | Понад 600 запитів за 60 с з одного IP на тенант |