AMIO для разработчиков
Подключите кассу, интернет-магазин или банковскую систему к AMIO. API дает те же результаты, что и работа в приложении, с теми же правилами, ролями и журналом аудита.
Аутентификация
Администратор вашей организации создает API-ключ в приложении, в разделе «Разработчикам». Передавайте его в заголовке Authorization: Bearer amio_live_... с каждым запросом к /api/public/v1. Ключ показывается только один раз и может быть отозван в любой момент.
API предназначен для серверов, а не для браузеров: CORS отключен, и ключ нельзя помещать в код, который выполняется на устройстве покупателя.
curl https://www.getamio.com/api/public/v1/quotes \
-H "Authorization: Bearer amio_live_..." \
-H "Idempotency-Key: 6f1c2f9e-6d0b-4f7e-9a51-2c8f2f4b7a10" \
-H "Content-Type: application/json" \
-d '{"amount":{"amountMinor":"12990000","currency":"RSD"},
"description":"Laptop","instalments":[6,12]}'Роли и права
У каждого ключа одна роль вашей организации, выбранная при создании, и он может только то, что эта роль может в приложении. Ключ может быть и только для чтения. Продавцы: администратор, менеджер и сотрудник; партнеры: администратор, утверждающий и аналитик.
Ключ действует от имени сотрудника, который его создал, и перестает работать, когда этот сотрудник покидает команду. Лимит 120 запросов в минуту на ключ; сверх этого вы получаете 429 и заголовок Retry-After.
Идемпотентность
Каждый POST и PUT должен содержать заголовок Idempotency-Key, например UUID. Повторный запрос с тем же ключом и тем же телом возвращает тот же ответ (с заголовком Idempotent-Replayed), поэтому предложение или возврат никогда не создаются дважды. Тот же ключ с другим телом возвращает 409 IDEMPOTENCY_CONFLICT. Ключи хранятся 24 часа.
Ошибки и суммы
Ошибки всегда одного вида: error.code, error.message и error.correlationId, тот же идентификатор есть в заголовке X-Correlation-Id. Укажите его при обращении в поддержку. Изменения используют версии: передавайте version, полученную последней; если что-то изменилось, вы получите 409 с причиной VERSION_CONFLICT.
Суммы всегда объект { amountMinor, currency }, где amountMinor целое число в наименьшей единице валюты, записанное как текст, например "12990000" для 129 900,00 RSD.
{
"error": {
"code": "CONFLICT",
"message": "VERSION_CONFLICT",
"reason": "VERSION_CONFLICT",
"correlationId": "c0a8..."
}
}Webhook-уведомления
Администратор добавляет адрес и выбирает события. AMIO отправляет POST с JSON-телом { id, type, createdAt, livemode, data } при изменении статуса. Ответьте статусом 2xx в течение 10 секунд; неудачные доставки повторяются с растущими интервалами, последние попытки видны в приложении.
У каждого сообщения есть заголовок AMIO-Signature: t=<unix-время>,v1=<hex HMAC-SHA256 от "t.тело">, подписанный секретом адреса. Проверяйте подпись по исходному телу и отклоняйте старые сообщения. Одно событие может прийти несколько раз; используйте id, чтобы обработать его один раз.
- Продавцы: quote.created, quote.status_changed, application.status_changed, refund.status_changed
- Партнеры: application.status_changed, deferral.requested, deferral.decided
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody: the request body exactly as received (a string), before JSON.parse.
export function verifyAmioWebhook(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(
String(header ?? "").split(",").map((part) => part.trim().split("=", 2)),
);
const timestamp = Number(parts.t);
if (!parts.v1 || !Number.isInteger(timestamp)) return false;
if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;
const expected = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest();
const given = Buffer.from(parts.v1, "hex");
return given.length === expected.length && timingSafeEqual(given, expected);
}
// Express: app.post("/amio/webhooks", express.text({ type: "*/*" }), (req, res) => {
// if (!verifyAmioWebhook(req.body, req.get("AMIO-Signature"), process.env.AMIO_WEBHOOK_SECRET))
// return res.sendStatus(400);
// const event = JSON.parse(req.body); // { id, type, createdAt, livemode, data }
// res.sendStatus(200);
// });Встроенные экраны
Экраны нового предложения и статуса предложения можно показать в вашем портале в iframe: /{locale}/embed/merchant/quotes/new и /{locale}/embed/merchant/quotes/{id}. Сначала администратор добавляет адрес портала; на всех остальных адресах браузер их блокирует (Content-Security-Policy: frame-ancestors).
Браузеры не передают cookie входа AMIO в iframe на другом сайте, поэтому ваш сервер запрашивает одноразовый токен: POST /api/public/v1/embed-tokens. Токен действует 5 минут, и при первом открытии AMIO меняет его на короткую сессию только для вашей компании, с ролью ключа. Если портал на том же сайте, что и AMIO, работает и обычный вход (откройте его во всплывающем окне).
// On your server, with a merchant key:
const response = await fetch("https://www.getamio.com/api/public/v1/embed-tokens", {
method: "POST",
headers: {
Authorization: "Bearer amio_live_...",
"Idempotency-Key": crypto.randomUUID(),
"Content-Type": "application/json",
},
body: JSON.stringify({ locale: "sr-Latn-RS" }), // or { quoteId } for one quote's status
});
const { url } = await response.json();
// In your portal page (an origin registered under Developers):
// <iframe src="{url}" width="480" height="820" title="AMIO"></iframe>Тестовая среда
Ключи, начинающиеся с amio_test_, существуют только в тестовой среде и работают там с тестовым партнером. В рабочей среде используются только ключи amio_live_. Предложения продавца, который еще не активен, всегда помечены как тестовые (mode SANDBOX, livemode false в webhook-сообщениях).