Download OpenAPI specification:
Public payments API of the PA Core payment aggregation platform.
The API exposes the PA model only (payments, refunds, payment instruments, payouts, merchant onboarding, audit trail, webhook delivery log, tenant key lifecycle). PSP/connector concepts never cross this boundary.
Conventions:
Authorization: Bearer <tenant_api_key> for every /v1 route (see securitySchemes.bearerAuth). Health probes and the docs portal are unauthenticated.pay_…, mer_…, pin_…, pout_…, tn_…, key_…).Idempotency-Key header; a retry with the same key and body replays the stored result (Idempotent-Replayed: true), a reused key with a different body is rejected with 422 idempotency_key_reuse.application/problem+json with stable PA code values; PSP error codes are never exposed.429 rate_limited with Retry-After.GET /docs (branded Redocly reference page) served by the same API instance; GET /docs/swagger-ui is the try-it-out console (Swagger UI); this document is GET /docs/openapi.json./docs reference page renders the operator handover guides INSIDE the sidebar «Guides» group — Handover Package, Onboarding Checklist, SDK + Webhooks (Secure Fields) and SDK README — built from the versioned repository documents by docs/api/build-redocly.sh; /docs is the single documentation entry point, the raw markdown surface stays at GET /portal/docs/{doc}.RU documentation of the contract lives in the repository under docs/api/ (Russian); this spec is the EN-facing reference for external merchants.
Merchant view: this page renders the MERCHANT-relevant endpoints only — the operator surfaces (admin console, PSP portal, ops tooling, audit trail, merchant onboarding, tenant keys, payouts) are part of the platform contract served by GET /docs/openapi.json and are intentionally omitted from this reference. The «Guides» group carries the integration documents; «Field Reference» details every field of POST /v1/payments.
Приветствуем! Вы получили доступ к платформе платежей PA: приём карточных депозитов (токенизация → платёж → подписанные webhooks → возвраты) через Payments API и Secure Fields SDK. Эта страница — самодостаточная точка входа: ключи, артефакты, 6 шагов интеграции, правила безопасности и ограничения текущего релиза. Детали каждого шага — в чек-листе онбординга казино (RU + EN).
Состояние платформы подтверждено полномочным acceptance-прогоном: платежи до AUTHORIZED
(Apcopay, saved-card tokenise → PURC CardToken), возвраты полный цикл
(PARTIALLY_REFUNDED/REFUNDED), webhooks exactly-once (дедуп переходов),
идемпотентность (422 idempotency_key_reuse), PCI: 0 PAN в логах/БД.
Поддержка (контакты заполняет Оператор при выдаче доступов): L1 — вопросы интеграции (), L2 — Оператор PA, дежурный (), L3 — PSP PayAdmit/Apcopay (___). Эскалация — чек-лист §7.
⚠️ Секреты выдаются ровно один раз. Значения передаются по защищённому каналу и показываются единожды — сразу сохраните их в свой secret store. Повторный показ невозможен; замена — только ротация через API (§4, правило 3).
| Параметр | Значение | Комментарий |
|---|---|---|
| Production base URL | https://payments.playpulse.tech |
Основной прод-адрес. TLS-сертификат Let's Encrypt, валиден до 2026-12-23, автопродление. Production ключи (tenant API key + whs_-секрет production MID) выдаются Оператором отдельно от sandbox-ключей — разделение сред |
| Sandbox base URL | https://sandbox.playpulse.tech |
Изолированный sandbox-инстанс. TLS-сертификат Let's Encrypt, валиден до 2026-12-27, автопродление. Legacy-алиас https://167-233-117-108.sslip.io действует до 2026-12-22 |
| Tenant API key (Bearer) | ___ |
Заголовок Authorization: Bearer <key> на каждый вызов PA API; raw-значение показывается один раз |
| Merchant ID | ___ (mer_…) |
Ваш профиль мерчанта в PA (валюты, маршрутизация, webhook) |
| Webhook secret | ___ (whs_…) |
Для проверки подписи PA-Webhook-Signature; показывается ОДИН раз при регистрации endpoint'а |
| captureUrl (для SDK) | ___ |
Выдаёт Оператор вместе с SDK-пакетом (значение captureUrl для PASecureFields.mount) |
| Артефакт | Где получить | Назначение |
|---|---|---|
| SDK frontend | https://<pa-origin>/pa-sdk/secure-fields.js (выдаёт Оператор в SDK-пакете) |
drop-in виджет: iframe-форма карты, onToken(vaultToken); без сборки |
| SDK iframe-страница | https://<pa-origin>/pa-sdk/secure-fields-frame.html |
размещается на origin PA; логику не менять |
| Демо-казино (эталон receiver'а) | SDK-пакет (demo/ — demo.html + server.js) |
рабочий frontend + backend + webhook receiver одним процессом |
| Payments API (RU) | гайд SDK + Webhooks + референс /docs |
/v1/payments, /v1/refunds, идемпотентность, ротация ключей |
| SDK + webhook контракт (RU) | гайд SDK + Webhooks | инструменты, HMAC v1, дедуп PA-Event-Id, ротация webhook-секрета |
| OpenAPI (EN) + референс | /docs/openapi.json · Redocly: /docs · Swagger UI: /docs/swagger-ui — всё на вашем base URL (раздел 1; корень / ведёт на /docs) |
машинная схема API |
| Чек-лист онбординга (RU + EN) | гайд Onboarding Checklist | детальные шаги К1–К6 / C1–C6, go-live §6, эскалация §7 |
| Field Reference | гайд Field Reference | все поля POST /v1/payments: типы, обязательность, ошибки |
| SDK README | гайд SDK README | «как это работает за 30 секунд» + локальный smoke |
| Глоссарий | гайд Термінологія | PA, PSP, MID, PURC, S2S, HPP, 3DS, PCI DSS, vault token… |
| Integration models | гайд Integration models | SDK / hosted page / S2S — модели и PCI DSS scope |
| Payouts | гайд Payouts — Phase 2 | модель выплат, статус поверхности |
Нумерация ниже — маршрут казино «от виджета до go-live». В чек-листе шаги детализированы своей нумерацией К1–К6; в скобках — куда смотреть за деталями.
<script src="https://<pa-origin>/pa-sdk/secure-fields.js"></script>
<div id="pa-card"></div>
<script>
const fields = PASecureFields.mount({
container: '#pa-card',
frameUrl: 'https://<pa-origin>/pa-sdk/secure-fields-frame.html',
captureUrl: '<из секции 1>', // выдаёт Оператор
onToken: (r) => {
// r.vaultToken — single-use vault token → сразу на СВОЙ backend
fetch('/my/api/deposit', { method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ vault_token: r.vaultToken, amount_cents: 1500 }) });
},
onError: (e) => showError(e.code, e.message),
});
</script>
Инварианты: iframe — opaque origin (JS казино не видит значения полей); vault_token —
одноразовый, не в URL/логи/localStorage. Тестовые карты PSP-sandbox выдаёт Оператор (чек-лист О6).
Endpoint регистрирует Оператор (секрет whs_… — §1). Эталонный Node/Express receiver:
const crypto = require('crypto');
const seen = new Set ; // в проде — постоянное хранилище дедупа (БД/Redis)
app.post('/pa/webhook',
express.raw({ type: '*/*' }), // СЫРОЕ тело — критично для подписи
(req, res) => {
const sig = String(req.get('PA-Webhook-Signature') || ''); // v1=<hex>
const expected = 'v1=' + crypto.createHmac('sha256', process.env.PA_WEBHOOK_SECRET).update(req.body).digest('hex');
const ok = sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
if (!ok) return res.sendStatus(401); // неверная подпись
const eventId = req.get('PA-Event-Id'); // дедуп: доставка at-least-once
if (seen.has(eventId)) return res.sendStatus(200);
seen.add(eventId);
const ev = JSON.parse(req.body); // {event_id, event_type, created_at, data{...}}
switch (ev.event_type) { // 4 типа событий
case 'payment.authorized':
case 'payment.captured': creditPlayer(ev.data); break; // зачисление депозита игроку
case 'payment.refunded': debitPlayer(ev.data); break;
case 'payment.failed': markFailed(ev.data); break;
}
res.sendStatus(200); // 2xx = доставлено (иначе retry PA)
});
Доставка at-least-once: retry 1м/5м/30м/2h, после 5 неуспешных — DEAD; отвечайте 2xx
быстро (таймаут PA — 5 с). Ротация секрета: в grace-окне (7 дней) верифицируйте оба секрета.
Два вызова с Idempotency-Key (uuid) и Authorization: Bearer <tenant API key>:
POST /v1/payment-instruments
Idempotency-Key: <uuid>
{"merchant_id": "mer_…", "vault_token": "<из К1>", "customer_reference": "usr_123"}
→ 201 {"instrument_id": "pin_…", "card_meta": {"masked_pan": "****1111", …}}
POST /v1/payments
Idempotency-Key: <uuid>
{"merchant_id": "mer_…", "amount": 1500, "currency": "EUR",
"payment_method": {"type": "card", "instrument_id": "pin_…"}, "capture_method": "automatic"}
→ 201 {"payment_id": "pay_…", "status": "PENDING"|"AUTHORIZED"|"FAILED", …}
Инструмент одноразовый (повтор → 422 instrument_not_active); PENDING → опрашивайте
GET /v1/payments/{id} (Retry-After); replay Idempotency-Key возвращает сохранённый
результат, тот же ключ с другим телом → 422 idempotency_key_reuse.
Если ответ содержит next_action: {"type": "redirect", "redirect_url": "…", "method": "GET"} —
переведите игрока по redirect_url. Возврат игрока на сайт — не результат: финальный
статус приходит webhook'ом (К2) и/или через GET /v1/payments/{id}.
POST /v1/refunds
Idempotency-Key: <uuid>
{"payment_id": "pay_…", "amount": 1050, "reason": "requested_by_customer"}
Частичные возвраты суммируются с контролем не-превышения AUTHORIZED-суммы; статусы
PARTIALLY_REFUNDED → REFUNDED приходят webhook'ом (событие payment.refunded, К2).
Полный цикл возвратов подтверждён acceptance-прогоном.
Запросите у Оператора production base URL и production ключи (tenant API key + whs_-секрет
production MID) и пройдите go-live чек-лист §6 (7 пунктов: полный цикл, HMAC, идемпотентность,
PCI-гигиена, ротации ключей, production MID ENABLED, мониторинг).
vault_token/pin_ и маскированные метаданные. PAN/CVV —
никогда в логи, БД, аудит (acceptance: 0 PAN).PA-Webhook-Signature: v1=<hex(HMAC-SHA256(raw_body, whs_секрет))> над сырым телом, сравнение timing-safe; без валидной подписи — не
обрабатывать (401). Дедуп по PA-Event-Id (= event_id payload, UUIDv7).POST /v1/tenants/{id}/rotate-key (новый сразу, старый 24 ч grace),
webhook-секрет POST /v1/merchants/{id}/rotate-webhook-secret (grace 7 дней, dual-verify).Idempotency-Key: <uuid> на каждый POST; повтор с тем же
ключом возвращает сохранённый результат (Idempotent-Replayed: true), тот же ключ с другим
телом → 422 idempotency_key_reuse. Webhook-replay по PA-Event-Id не должен дублировать
зачисление игроку.| Ограничение | Суть | Когда снимается |
|---|---|---|
| Production MID | Prod URL жив: https://payments.playpulse.tech. Production MID у PSP — в оформлении на стороне PSP; production ключи выдаются отдельно от sandbox |
у PSP |
| 3DS | PayAdmit — hosted HPP в их среде (в пилоте demo 502); Apcopay — NotSupported в MVP |
отдельный этап roadmap |
| Payouts | /v1/payouts не входит в текущий релиз |
Phase 2 (AM Q6) |
| KYC | Не входит в MVP — средствами казино/партнёров | вне скоупа PA |
Welcome — you get sandbox API access, the Secure Fields SDK and signed webhooks. Integration in
6 steps: K1 mount the SDK iframe (onToken(vaultToken) → send straight to your backend);
K2 run a webhook receiver — verify PA-Webhook-Signature (v1=<hex HMAC-SHA256> over the
raw body), dedup by PA-Event-Id, reply 2xx fast; K3 exchange the vault token
(POST /v1/payment-instruments → pin_…) and create the payment (POST /v1/payments with
Idempotency-Key on the sandbox base URL from section 1); K4 follow
next_action.redirect (3DS / hosted page) — the final status arrives via webhook and/or
GET /v1/payments/{id}; K5 refunds via POST /v1/refunds (partial refunds supported,
PARTIALLY_REFUNDED → REFUNDED); K6 go-live — request the production URL and keys from the
Operator and pass the go-live checklist. Security: PAN/CVV never leave the SDK iframe; HMAC
verification and idempotency are mandatory; keep secrets out of git and logs.
Limits: production URL is live (https://payments.playpulse.tech) — production MID
at the PSP is pending with the PSP, production keys are issued separately from sandbox,
3DS limited (PayAdmit hosted HPP / Apcopay NotSupported in MVP), payouts = Phase 2, no KYC in MVP.
Details: onboarding checklist, English Part II.
/docs (Redocly) и Swagger UI
/docs/swagger-ui на вашем base URL (раздел 1).4111 1111 1111 1111 (12/30, CVC 123), 5555 5555 5555 4444 (MC),
4000 0000 0000 0002 (3DS).Приветствуем! Вы получили доступ к платформе платежей PA: приём карточных депозитов (токенизация → платёж → подписанные webhooks → возвраты) через Payments API и Secure Fields SDK. Эта страница — самодостаточная точка входа: ключи, артефакты, 6 шагов интеграции, правила безопасности и ограничения текущего релиза. Детали каждого шага — в чек-листе онбординга казино (RU + EN).
Состояние платформы подтверждено полномочным acceptance-прогоном: платежи до AUTHORIZED
(Apcopay, saved-card tokenise → PURC CardToken), возвраты полный цикл
(PARTIALLY_REFUNDED/REFUNDED), webhooks exactly-once (дедуп переходов),
идемпотентность (422 idempotency_key_reuse), PCI: 0 PAN в логах/БД.
Поддержка (контакты заполняет Оператор при выдаче доступов): L1 — вопросы интеграции (), L2 — Оператор PA, дежурный (), L3 — PSP PayAdmit/Apcopay (___). Эскалация — чек-лист §7.
⚠️ Секреты выдаются ровно один раз. Значения передаются по защищённому каналу и показываются единожды — сразу сохраните их в свой secret store. Повторный показ невозможен; замена — только ротация через API (§4, правило 3).
| Параметр | Значение | Комментарий |
|---|---|---|
| Production base URL | https://payments.playpulse.tech |
Основной прод-адрес. TLS-сертификат Let's Encrypt, валиден до 2026-12-23, автопродление. Production ключи (tenant API key + whs_-секрет production MID) выдаются Оператором отдельно от sandbox-ключей — разделение сред |
| Sandbox base URL | https://sandbox.playpulse.tech |
Изолированный sandbox-инстанс. TLS-сертификат Let's Encrypt, валиден до 2026-12-27, автопродление. Legacy-алиас https://167-233-117-108.sslip.io действует до 2026-12-22 |
| Tenant API key (Bearer) | ___ |
Заголовок Authorization: Bearer <key> на каждый вызов PA API; raw-значение показывается один раз |
| Merchant ID | ___ (mer_…) |
Ваш профиль мерчанта в PA (валюты, маршрутизация, webhook) |
| Webhook secret | ___ (whs_…) |
Для проверки подписи PA-Webhook-Signature; показывается ОДИН раз при регистрации endpoint'а |
| captureUrl (для SDK) | ___ |
Выдаёт Оператор вместе с SDK-пакетом (значение captureUrl для PASecureFields.mount) |
| Артефакт | Где получить | Назначение |
|---|---|---|
| SDK frontend | https://<pa-origin>/pa-sdk/secure-fields.js (выдаёт Оператор в SDK-пакете) |
drop-in виджет: iframe-форма карты, onToken(vaultToken); без сборки |
| SDK iframe-страница | https://<pa-origin>/pa-sdk/secure-fields-frame.html |
размещается на origin PA; логику не менять |
| Демо-казино (эталон receiver'а) | SDK-пакет (demo/ — demo.html + server.js) |
рабочий frontend + backend + webhook receiver одним процессом |
| Payments API (RU) | гайд SDK + Webhooks + референс /docs |
/v1/payments, /v1/refunds, идемпотентность, ротация ключей |
| SDK + webhook контракт (RU) | гайд SDK + Webhooks | инструменты, HMAC v1, дедуп PA-Event-Id, ротация webhook-секрета |
| OpenAPI (EN) + референс | /docs/openapi.json · Redocly: /docs · Swagger UI: /docs/swagger-ui — всё на вашем base URL (раздел 1; корень / ведёт на /docs) |
машинная схема API |
| Чек-лист онбординга (RU + EN) | гайд Onboarding Checklist | детальные шаги К1–К6 / C1–C6, go-live §6, эскалация §7 |
| Field Reference | гайд Field Reference | все поля POST /v1/payments: типы, обязательность, ошибки |
| SDK README | гайд SDK README | «как это работает за 30 секунд» + локальный smoke |
| Глоссарий | гайд Термінологія | PA, PSP, MID, PURC, S2S, HPP, 3DS, PCI DSS, vault token… |
| Integration models | гайд Integration models | SDK / hosted page / S2S — модели и PCI DSS scope |
| Payouts | гайд Payouts — Phase 2 | модель выплат, статус поверхности |
Нумерация ниже — маршрут казино «от виджета до go-live». В чек-листе шаги детализированы своей нумерацией К1–К6; в скобках — куда смотреть за деталями.
<script src="https://<pa-origin>/pa-sdk/secure-fields.js"></script>
<div id="pa-card"></div>
<script>
const fields = PASecureFields.mount({
container: '#pa-card',
frameUrl: 'https://<pa-origin>/pa-sdk/secure-fields-frame.html',
captureUrl: '<из секции 1>', // выдаёт Оператор
onToken: (r) => {
// r.vaultToken — single-use vault token → сразу на СВОЙ backend
fetch('/my/api/deposit', { method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ vault_token: r.vaultToken, amount_cents: 1500 }) });
},
onError: (e) => showError(e.code, e.message),
});
</script>
Инварианты: iframe — opaque origin (JS казино не видит значения полей); vault_token —
одноразовый, не в URL/логи/localStorage. Тестовые карты PSP-sandbox выдаёт Оператор (чек-лист О6).
Endpoint регистрирует Оператор (секрет whs_… — §1). Эталонный Node/Express receiver:
const crypto = require('crypto');
const seen = new Set ; // в проде — постоянное хранилище дедупа (БД/Redis)
app.post('/pa/webhook',
express.raw({ type: '*/*' }), // СЫРОЕ тело — критично для подписи
(req, res) => {
const sig = String(req.get('PA-Webhook-Signature') || ''); // v1=<hex>
const expected = 'v1=' + crypto.createHmac('sha256', process.env.PA_WEBHOOK_SECRET).update(req.body).digest('hex');
const ok = sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
if (!ok) return res.sendStatus(401); // неверная подпись
const eventId = req.get('PA-Event-Id'); // дедуп: доставка at-least-once
if (seen.has(eventId)) return res.sendStatus(200);
seen.add(eventId);
const ev = JSON.parse(req.body); // {event_id, event_type, created_at, data{...}}
switch (ev.event_type) { // 4 типа событий
case 'payment.authorized':
case 'payment.captured': creditPlayer(ev.data); break; // зачисление депозита игроку
case 'payment.refunded': debitPlayer(ev.data); break;
case 'payment.failed': markFailed(ev.data); break;
}
res.sendStatus(200); // 2xx = доставлено (иначе retry PA)
});
Доставка at-least-once: retry 1м/5м/30м/2h, после 5 неуспешных — DEAD; отвечайте 2xx
быстро (таймаут PA — 5 с). Ротация секрета: в grace-окне (7 дней) верифицируйте оба секрета.
Два вызова с Idempotency-Key (uuid) и Authorization: Bearer <tenant API key>:
POST /v1/payment-instruments
Idempotency-Key: <uuid>
{"merchant_id": "mer_…", "vault_token": "<из К1>", "customer_reference": "usr_123"}
→ 201 {"instrument_id": "pin_…", "card_meta": {"masked_pan": "****1111", …}}
POST /v1/payments
Idempotency-Key: <uuid>
{"merchant_id": "mer_…", "amount": 1500, "currency": "EUR",
"payment_method": {"type": "card", "instrument_id": "pin_…"}, "capture_method": "automatic"}
→ 201 {"payment_id": "pay_…", "status": "PENDING"|"AUTHORIZED"|"FAILED", …}
Инструмент одноразовый (повтор → 422 instrument_not_active); PENDING → опрашивайте
GET /v1/payments/{id} (Retry-After); replay Idempotency-Key возвращает сохранённый
результат, тот же ключ с другим телом → 422 idempotency_key_reuse.
Если ответ содержит next_action: {"type": "redirect", "redirect_url": "…", "method": "GET"} —
переведите игрока по redirect_url. Возврат игрока на сайт — не результат: финальный
статус приходит webhook'ом (К2) и/или через GET /v1/payments/{id}.
POST /v1/refunds
Idempotency-Key: <uuid>
{"payment_id": "pay_…", "amount": 1050, "reason": "requested_by_customer"}
Частичные возвраты суммируются с контролем не-превышения AUTHORIZED-суммы; статусы
PARTIALLY_REFUNDED → REFUNDED приходят webhook'ом (событие payment.refunded, К2).
Полный цикл возвратов подтверждён acceptance-прогоном.
Запросите у Оператора production base URL и production ключи (tenant API key + whs_-секрет
production MID) и пройдите go-live чек-лист §6 (7 пунктов: полный цикл, HMAC, идемпотентность,
PCI-гигиена, ротации ключей, production MID ENABLED, мониторинг).
vault_token/pin_ и маскированные метаданные. PAN/CVV —
никогда в логи, БД, аудит (acceptance: 0 PAN).PA-Webhook-Signature: v1=<hex(HMAC-SHA256(raw_body, whs_секрет))> над сырым телом, сравнение timing-safe; без валидной подписи — не
обрабатывать (401). Дедуп по PA-Event-Id (= event_id payload, UUIDv7).POST /v1/tenants/{id}/rotate-key (новый сразу, старый 24 ч grace),
webhook-секрет POST /v1/merchants/{id}/rotate-webhook-secret (grace 7 дней, dual-verify).Idempotency-Key: <uuid> на каждый POST; повтор с тем же
ключом возвращает сохранённый результат (Idempotent-Replayed: true), тот же ключ с другим
телом → 422 idempotency_key_reuse. Webhook-replay по PA-Event-Id не должен дублировать
зачисление игроку.| Ограничение | Суть | Когда снимается |
|---|---|---|
| Production MID | Prod URL жив: https://payments.playpulse.tech. Production MID у PSP — в оформлении на стороне PSP; production ключи выдаются отдельно от sandbox |
у PSP |
| 3DS | PayAdmit — hosted HPP в их среде (в пилоте demo 502); Apcopay — NotSupported в MVP |
отдельный этап roadmap |
| Payouts | /v1/payouts не входит в текущий релиз |
Phase 2 (AM Q6) |
| KYC | Не входит в MVP — средствами казино/партнёров | вне скоупа PA |
Welcome — you get sandbox API access, the Secure Fields SDK and signed webhooks. Integration in
6 steps: K1 mount the SDK iframe (onToken(vaultToken) → send straight to your backend);
K2 run a webhook receiver — verify PA-Webhook-Signature (v1=<hex HMAC-SHA256> over the
raw body), dedup by PA-Event-Id, reply 2xx fast; K3 exchange the vault token
(POST /v1/payment-instruments → pin_…) and create the payment (POST /v1/payments with
Idempotency-Key on the sandbox base URL from section 1); K4 follow
next_action.redirect (3DS / hosted page) — the final status arrives via webhook and/or
GET /v1/payments/{id}; K5 refunds via POST /v1/refunds (partial refunds supported,
PARTIALLY_REFUNDED → REFUNDED); K6 go-live — request the production URL and keys from the
Operator and pass the go-live checklist. Security: PAN/CVV never leave the SDK iframe; HMAC
verification and idempotency are mandatory; keep secrets out of git and logs.
Limits: production URL is live (https://payments.playpulse.tech) — production MID
at the PSP is pending with the PSP, production keys are issued separately from sandbox,
3DS limited (PayAdmit hosted HPP / Apcopay NotSupported in MVP), payouts = Phase 2, no KYC in MVP.
Details: onboarding checklist, English Part II.
/docs (Redocly) и Swagger UI
/docs/swagger-ui на вашем base URL (раздел 1).4111 1111 1111 1111 (12/30, CVC 123), 5555 5555 5555 4444 (MC),
4000 0000 0000 0002 (3DS).Одна страница (RU + EN). Роли: Оператор PA / Казино. Связанные гайды: SDK + Webhooks, SDK README, Field Reference, Handover Package, Термінологія, Integration models.
English version: Part II ниже.
Одна страница, чтобы провести казино от контракта до go-live: Оператор PA настраивает конфигурацию на стороне платформы, Казино (их разработчики) интегрируют SDK и Payments API. Параллельные треки — таблица §3; детали — §4 (О1–О6) и §5 (К1–К6).
| Роль | Кто | Ключ доступа | Зона ответственности |
|---|---|---|---|
| Оператор PA | наша сторона | admin Bearer = tenant API key (аудит-актор tenant:<tenant_id>) |
§4: О1–О6 (конфигурация, MID, webhook, выдача доступов, production) |
| Казино | разработчики казино | тот же tenant Bearer для backend + SDK на фронтенде | §5: К1–К6 (SDK iframe, инструменты, платежи, webhook receiver, redirect) |
Base URLs. Sandbox — URL конкретного развёртывания подтверждает оператор при выдаче
доступов (референсный sandbox: https://sandbox.playpulse.tech). Production base URL —
отдельно, выдаётся на go-live.
Общие правила вызовов: все POST-операции платежей — с заголовком Idempotency-Key: <uuid>;
ошибки — RFC 7807 application/problem+json (машинный дискриминатор — поле code);
429 rate_limited → повторять после Retry-After.
Оператор (до начала):
payadmit | apcopay) заведён на стороне PA → создан MCA → известен
credentials_ref; секреты PSP PA не хранит;GET /v1/audit (аудит-след своих операций).Казино (до начала):
| Этап | Оператор PA (мы) | Казино (их разработчики) |
|---|---|---|
| Настройка PA | О1 профиль → О2 PSP+MID → О3 маршрут → О4 webhook | — |
| Доступы и SDK | О5 выдать tenant API key, SDK-пакет, captureUrl, доки | К1 принять доступы, подключить secure-fields.js |
| Интеграция казино | — | К2 инструменты (pin_) → К3 платежи (pay_) → К4 webhook receiver → К5 redirect |
| Sandbox | О6 сопровождение полного цикла | совместный smoke: полный платёж + 4 типа webhook |
| Go-live | О6: production MID ENABLED | К6: production base URL + ключи, чек-лист §6 |
Все вызовы аутентифицируются Bearer-ключом тенанта:
Authorization: Bearer <tenant_api_key>
merchant_idPOST /v1/merchants
{
"name": "Alpha Casino",
"country": "MT",
"currencies": ["EUR", "USD"],
"legal_entity": {"name": "Alpha Ltd", "registration": "C-007"}
}
→ 201: {"merchant_id": "mer_…", "name": "Alpha Casino", "currencies": ["EUR","USD"], …}.
currencies — дефолтный валютный скоуп маршрутизации. Аудит: create_merchant.
Проверка: GET /v1/merchants/{id} — полная конфигурация (профиль + провайдеры + Route Targets).
provider_id / provider_account_id{
"provider_code": "apcopay", // payadmit | apcopay
"mid": "apcopay_sandbox_pa56",
"credentials_ref": "mca_CqaSkqlnP97buXqrJy1u",
"currencies": ["EUR"],
"countries": ["MT"]
}
→ 201: {"provider_id": "prv_…", "provider_account_id": "mid_…", "connector_code": "apcopay", "mid_label": "…"}.
Один вызов = один MID; credentials_ref — ссылка на Merchant Connector Account на стороне
fork (Hyperswitch); секреты PSP PA не хранит. Повторный MID того же коннектора переиспользует
провайдера (уникальность (merchant_id, connector_code)).
rt_id{
"provider_account_id": "mid_…",
"priority": 1,
"weight": 5,
"filters": {"country": "MT", "currency": ["EUR"], "method": "automatic"}
}
→ 201: {"route_target_id": "rt_…", …} — мишень активна в каскаде.
Обязательно: filters.currency и filters.method (automatic | manual) — мишень без
них отклоняется (422 invalid_request, «мёртвый маршрут»). Порядок каскада: priority ASC,
затем weight DESC. Аудит: update_routing.
whs_ (показан ОДИН раз){"url": "https://casino.example/pa/webhook"}
→ ответ содержит webhook_secret: whs_… — показывается ровно один раз, передать казино
по защищённому каналу; аудит set_merchant_webhook (секрет в details не пишется).
GET /v1/merchants/{id} возвращает только webhook_url. Ротация секрета:
POST /v1/merchants/{id}/rotate-webhook-secret (grace 7 дней, dual-verify на стороне казино).
POST /v1/tenants/{id}/rotate-key
(новый ключ валиден немедленно, старый живёт 24 ч — grace period; raw показывается один раз);pa-sdk/secure-fields.js + pa-sdk/secure-fields-frame.html (drop-in, без
сборки; frame размещается на origin PA) + значение captureUrl для SDK;pa-sdk/README.md;AUTHORIZED/CAPTURED), webhook доставлен (2xx) — журнал: GET /v1/webhooks/deliveries?payment_id=pay_…;4111 1111 1111 1111
(12/30, CVC 123), 5555 5555 5555 4444 (MC), 4000 0000 0000 0002 (3DS);GET /v1/audit?entity_type=merchant&entity_id=mer_…;<script src="https://<pa-origin>/pa-sdk/secure-fields.js"></script>
<div id="pa-card"></div>
<script>
const fields = PASecureFields.mount({
container: '#pa-card',
frameUrl: 'https://<pa-origin>/pa-sdk/secure-fields-frame.html',
captureUrl: '<capture endpoint>', // выдаёт Оператор при интеграции; 'demo' — тестовый режим
onToken: (r) => {
// r.vaultToken — single-use vault token → сразу на СВОЙ backend
fetch('/my/api/deposit', { method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ vault_token: r.vaultToken, amount_cents: 1500 }) });
},
onError: (e) => showError(e.code, e.message),
});
</script>
Инварианты безопасности (не нарушать):
sandbox="allow-scripts allow-forms" — БЕЗ allow-same-origin
(opaque origin: casino JS не имеет DOM-доступа к значениям полей);pa-secure-fields);vault_token — одноразовый credential: не в URL, не в логи, не в localStorage;
регистрируется ровно один раз (409 vault_token_already_registered при повторе).POST /v1/payment-instruments
Idempotency-Key: <uuid>
Authorization: Bearer <tenant_api_key>
{"merchant_id": "mer_…", "vault_token": "<из onToken>", "customer_reference": "usr_123"}
→ 201: {"instrument_id": "pin_…", "card_meta": {"masked_pan": "****1111", "brand": "VISA", …}, "status": "ACTIVE"}.
Сохраните пару (customer_reference, instrument_id). Ограничения customer_reference:
≤ 255 символов, charset [A-Za-z0-9_.@-], PAN-побы отвергаются (422 invalid_request).
Ошибки: vault_token_unknown (токен потрачен — запросите карту заново),
vault_unavailable 502 (повторить с новым Idempotency-Key),
vault_token_already_registered 409.
POST /v1/payments
Idempotency-Key: <uuid>
Authorization: Bearer <tenant_api_key>
{
"merchant_id": "mer_…",
"amount": 1500, // минорные единицы (15.00 EUR)
"currency": "EUR",
"payment_method": {"type": "card", "instrument_id": "pin_…"},
"capture_method": "automatic"
}
→ 201: {"payment_id": "pay_…", "status": "PENDING"|"AUTHORIZED"|"FAILED", "next_action": …, "attempts_summary": […]}.
pin_ → 422 instrument_not_active;PENDING → опрашивайте GET /v1/payments/{id} (Retry-After);
UNKNOWN_PENDING_SYNC — не failed: идёт sync, дождитесь финала;Idempotency-Key не создаёт новый платёж — возвращает
сохранённый результат.Регистрацию endpoint'а выполняет Оператор (О4); секрет whs_… казино получает от Оператора.
Обработка на стороне казино:
// Node/Express — эталонный receiver
const crypto = require('crypto');
const seen = new Set ; // в проде — постоянное хранилище дедупа (БД/Redis)
app.post('/pa/webhook',
express.raw({ type: '*/*' }), // СЫРОЕ тело — критично для подписи
(req, res) => {
const sig = String(req.get('PA-Webhook-Signature') || ''); // v1=<hex>
const expected = 'v1=' + crypto.createHmac('sha256', process.env.PA_WEBHOOK_SECRET).update(req.body).digest('hex');
const ok = sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
if (!ok) return res.sendStatus(401); // неверная подпись
const eventId = req.get('PA-Event-Id'); // дедуп: доставка at-least-once
if (seen.has(eventId)) return res.sendStatus(200);
seen.add(eventId);
const ev = JSON.parse(req.body); // {event_id, event_type, created_at, data{...}}
switch (ev.event_type) { // 4 типа событий
case 'payment.authorized':
case 'payment.captured': creditPlayer(ev.data); break; // зачисление депозита игроку
case 'payment.refunded': debitPlayer(ev.data); break;
case 'payment.failed': markFailed(ev.data); break;
}
res.sendStatus(200); // 2xx = доставлено (иначе retry PA)
});
Контракт доставки:
PA-Webhook-Signature: v1=<hex(HMAC-SHA256(raw_body, whs_секрет))> над сырым
телом; PA-Event-Id: <event_id> (= payload event_id, UUIDv7 — ключ дедупликации);payment.authorized, payment.captured, payment.refunded, payment.failed
(PENDING/UNKNOWN_PENDING_SYNC не доставляются);DEAD
(журнал доставок у Оператора: GET /v1/webhooks/deliveries?payment_id=pay_…);
отвечайте 2xx быстро (таймаут PA — 5 с по умолчанию);old_secret_expires_at).Если POST /v1/payments или GET /v1/payments/{id} вернул
next_action: {"type": "redirect", "redirect_url": "…", "method": "GET"} — переведите игрока
по redirect_url (3DS challenge / hosted page PSP). Возврат игрока на сайт — не результат:
финальный статус приходит webhook'ом (К4) и/или через GET /v1/payments/{id}
(после возврата next_action = {type: "wait", retry_after} до финала).
Запросить у Оператора production base URL и production ключи (tenant API key + whs_-секрет
production MID; ротации — О5/О4), пройти чек-лист §6.
Отметка ответственного: [О] — Оператор PA, [К] — Казино.
AUTHORIZED/CAPTURED) — полный циклPA-Webhook-Signature (HMAC-SHA256 по сырому телу) работает на receiver'е — все 4 типа событий: payment.authorized / payment.captured / payment.refunded / payment.failedIdempotency-Key возвращает сохранённый результат; тот же ключ с другим телом → 422 idempotency_key_reuse; webhook-replay по PA-Event-Id не дублирует зачислениеpin_/pay_ и маскированные метаданныеPOST /v1/tenants/{id}/rotate-key) и webhook-секрета (POST /v1/merchants/{id}/rotate-webhook-secret); raw-значения переданы в secret store казино| Уровень | Кому | Когда |
|---|---|---|
| L1 | разработчики казино | вопросы интеграции SDK/API, receiver |
| L2 | Оператор PA (наш дежурный) | 401/404/422 от PA API, routing/eligibility (no_eligible_route), доставка webhook (DEAD), production MID |
| L3 | PSP (PayAdmit / Apcopay) | decline'ы PSP, инциденты 3DS/hosted page, доступность PSP |
Контакты фиксируются Оператором при выдаче production-доступов (заполнить перед go-live).
Инструменты диагностики: GET /v1/webhooks/deliveries?payment_id=… (журнал доставок),
GET /v1/payments/{id} (состояние), GET /v1/audit (оператор), GET /healthz (живость PA).
/v1/payment-instruments, webhook-контракт;POST /v1/payments, сводка ошибок;AUTHORIZED / CAPTURED / PENDING / FAILED / UNKNOWN_PENDING_SYNC.A single page to take a casino from contract to go-live: the PA Operator configures the PA Core side, the Casino (their developers) integrates the SDK and the Payments API.
| Role | Who | Access | Responsibility |
|---|---|---|---|
| PA Operator | our side | admin Bearer = tenant API key (audit actor tenant:<tenant_id>) |
Steps O1–O6: configuration, MID, webhook, credential handover, production |
| Casino | casino dev team | same tenant Bearer for backend + SDK on the frontend | Steps C1–C6: SDK iframe, instruments, payments, webhook receiver, redirect |
Base URLs. Sandbox — the URL of a given deployment is confirmed by the Operator when
access is granted (reference sandbox: https://sandbox.playpulse.tech). The production base
URL is handed over separately at go-live. All payment POSTs require Idempotency-Key: <uuid>;
errors are RFC 7807 (code is the machine discriminator); 429 rate_limited → retry after
Retry-After.
Operator: tenant created (bootstrap/seed); PSP account (payadmit | apcopay) configured
on the PA side with an MCA → credentials_ref;
access to the sandbox PA API and GET /v1/audit.
Casino: HTTPS webhook-receiver endpoint (http is allowed in dev only; HTTPS is mandatory in
production); a backend able to make 2–3 PA API calls and receive signed webhooks; a deposit
page (frontend) for the SDK; PSP-sandbox test cards (see O6).
| Stage | PA Operator (us) | Casino (their devs) |
|---|---|---|
| PA configuration | O1 merchant → O2 PSP+MID → O3 route → O4 webhook | — |
| Credentials & SDK | O5 hand over tenant API key, SDK bundle, captureUrl, docs | C1 accept credentials, mount secure-fields.js |
| Casino integration | — | C2 instruments (pin_) → C3 payments (pay_) → C4 webhook receiver → C5 redirect |
| Sandbox | O6 accompany the full cycle | joint smoke run: full payment + 4 webhook types |
| Go-live | O6: production MID ENABLED | C6: production base URL + keys, checklist §6 |
All calls are authenticated with the tenant Bearer key. Steps mirror §4 of Part I — same payloads and responses:
POST /v1/merchants — casino profile {name, country, currencies[], legal_entity}
→ 201 with merchant_id (mer_…); audit create_merchant; verify via GET /v1/merchants/{id}.POST /v1/merchants/{id}/providers — {provider_code: payadmit|apcopay, mid, credentials_ref, currencies, countries} → 201 with provider_id (prv_…) and
provider_account_id (mid_…). credentials_ref points to the PSP connector account (MCA) on the PA side;
PA never stores PSP secrets.POST /v1/merchants/{id}/route-targets — {provider_account_id, priority, weight, filters: {country, currency, method}} → 201 with route_target_id (rt_…).
filters.currency and filters.method are mandatory (otherwise 422 — a "dead route").
Cascade order: priority ASC, then weight DESC.POST /v1/merchants/{id}/webhook — {"url": "https://casino.example/pa/webhook"}
→ response contains webhook_secret (whs_…) shown exactly once; hand it to the casino
over a secure channel. Rotation: POST /v1/merchants/{id}/rotate-webhook-secret
(7-day grace, dual-verify on the casino side).POST /v1/tenants/{id}/rotate-key, 24 h grace), SDK bundle (pa-sdk/secure-fields.js +
secure-fields-frame.html), the captureUrl value, docs (docs/api/*, pa-sdk/README.md),
PSP-sandbox test cards.AUTHORIZED/CAPTURED, webhook
delivered — journal GET /v1/webhooks/deliveries?payment_id=…), then production MID
(ENABLED) → go-live checklist §6.PASecureFields.mount(...) snippet from §5/К1
(Part I): container, frameUrl (PA origin, /pa-sdk/secure-fields-frame.html),
captureUrl, onToken → send vault_token to your own backend. Security invariants:
iframe sandbox="allow-scripts allow-forms" without allow-same-origin; PAN/CVV are
typed inside the iframe and go directly to the capture surface (vault) — PA sees only the
vault token, the casino backend never sees PAN/CVV; postMessage with origin checks both ways;
vault_token is a single-use credential — never in URLs/logs/localStorage.POST /v1/payment-instruments with
Idempotency-Key, body {merchant_id, vault_token, customer_reference} → 201 with
instrument_id (pin_…) and masked card_meta. Errors: vault_token_unknown (token spent —
ask for the card again), vault_unavailable 502 (retry with a new Idempotency-Key),
vault_token_already_registered 409.POST /v1/payments with Idempotency-Key, body
{merchant_id, amount (minor units), currency, payment_method: {type: "card", instrument_id}, capture_method: "automatic"} → 201 with payment_id (pay_…), status
(PENDING|AUTHORIZED|FAILED), optional next_action. The instrument is single-use
(re-use → 422 instrument_not_active); PENDING → poll GET /v1/payments/{id}
(Retry-After); UNKNOWN_PENDING_SYNC is not failed — wait for the sync result; replaying
the same Idempotency-Key returns the stored result (same payment_id), a different body
with the same key → 422 idempotency_key_reuse.whs_… secret. On every delivery: verify
PA-Webhook-Signature: v1=<hex(HMAC-SHA256(raw_body, whs_secret))> over the raw body
(reference receiver code — §5/К4), deduplicate by PA-Event-Id (= payload event_id,
UUIDv7; delivery is at-least-once: retries 1m/5m/30m/2h, then DEAD), handle the four event
types payment.authorized / payment.captured / payment.refunded / payment.failed, and
respond 2xx quickly. During secret rotation verify both secrets until
old_secret_expires_at.next_action.type = "redirect", send the
player to redirect_url (3DS challenge / hosted page). The player's return is not the
result: the final status arrives via webhook (C4) and/or GET /v1/payments/{id}.Marking: [O] — PA Operator, [C] — Casino.
AUTHORIZED/CAPTURED) — full cycleIdempotency-Key returns the stored result (same payment_id,
Idempotent-Replayed: true); same key with a different body → 422 idempotency_key_reuse;
webhook replay by PA-Event-Id does not double-creditpin_/pay_ and masked metadataPOST /v1/tenants/{id}/rotate-key) and webhook secret
rotation (POST /v1/merchants/{id}/rotate-webhook-secret); raw values stored in a secret store| Level | Who | When |
|---|---|---|
| L1 | casino dev team | SDK/API integration questions, receiver |
| L2 | PA Operator (our on-call) | 401/404/422 from PA API, routing/eligibility (no_eligible_route), webhook delivery DEAD, production MID |
| L3 | PSP (PayAdmit / Apcopay) | PSP declines, 3DS/HPP incidents, PSP availability |
Contacts are fixed by the Operator when production access is granted (fill in before go-live).
Diagnostics: GET /v1/webhooks/deliveries?payment_id=…, GET /v1/payments/{id}, GET /v1/audit
(operator), /healthz.
Same as §8 of Part I: Handover Package, SDK + Webhooks, Field Reference, SDK README, Термінологія.
Справочник терминов и сокращений Payments API для казино-разработчика. Каждый термин — «что означает для казино»: где вы с ним столкнётесь при интеграции (см. Handover Package и чек-лист онбординга).
| Термин | Расшифровка | Что означает для казино |
|---|---|---|
| PA | Payment Aggregator — платёжная платформа-агрегатор | Платформа, к которой вы интегрируетесь: Payments API, Secure Fields SDK, webhooks, мерчант-портал |
| PSP | Payment Service Provider — платёжный провайдер | Внешний процессор карточных платежей (PayAdmit, Apcopay). PA направляет платежи на PSP через маршрутизацию; decline'ы и 3DS приходят именно от PSP |
| MID | Merchant Identifier — идентификатор мерчанта у PSP | Ваш аккаунт у конкретного PSP. Создаётся и привязывается Оператором (вы получаете merchant_id в PA, MID — «за кадром»); статус MID определяет, идут ли ваши платежи в проде |
| PURC | Purchase — операция покупки | Основная карточная операция депозита: при capture_method: "automatic" PA диспатчит PURC в PSP — авторизация и захват одним вызовом. Успешный PURC = статус AUTHORIZED = можно кредитовать игрока |
| S2S | Server-to-Server — платёж сервер-сервер | Модель, где ваш backend шлёт платёжный запрос напрямую в API PA (в отличие от redirect-модели). Текущий основной флоу: backend + Secure Fields SDK на фронте |
| HPP | Hosted Payment Page — хостед-страница оплаты | Страница оплаты, размещённая у PSP: игрок перенаправляется на неё (next_action: redirect) и вводит данные там. У PayAdmit — модель hosted HPP; в PA — Phase 2 (см. Integration models) |
| 3DS | 3-D Secure — протокол аутентификации держателя карты | Дополнительное подтверждение платежа игроком (challenge на стороне банка/PSP). Если PSP требует 3DS, платёж отвечает next_action: {type: "redirect"} — игрока надо отправить по redirect_url, финал придёт webhook'ом |
| PCI DSS | Payment Card Industry Data Security Standard | Стандарт безопасности карточных данных. Следствие для вас: PAN/CVV должны вводиться только в SDK iframe — ваш backend остаётся вне PCI DSS scope (см. Integration models) |
| vault token | Одноразовый токен токенизации карты | Единственное, что выходит из SDK iframe вместо PAN/CVV (onToken(vaultToken)). Одноразовый, не хранится в логах/localStorage, обменивается на инструмент через POST /v1/payment-instruments |
| next_action | Поле ответа «что делать дальше» | Инструкция клиенту: none / wait / redirect / collect_data / approval. Семантика каждого значения — Field Reference §3.1 |
| capture | Захват авторизованной суммы | При automatic захват выполняется PSP вместе с авторизацией (sale-модель); при manual платёж ждёт отдельного захвата (в текущем публичном API capture-endpoint не экспонирован — используйте automatic) |
| redirect_url | URL из next_action: redirect |
Адрес, на который надо перевести игрока (3DS challenge / hosted page PSP). Возврат игрока на сайт — не результат платежа: финальный статус приходит webhook'ом и/или GET /v1/payments/{id} |
| webhook | Подписанный HTTP-callback о событии платежа | PA доставляет payment.authorized / payment.captured / payment.refunded / payment.failed на ваш endpoint; верифицируйте PA-Webhook-Signature, дедуплицируйте по PA-Event-Id, отвечайте 2xx |
| Idempotency-Key | Заголовок идемпотентности POST-запросов | Обязателен на каждый POST платежей/возвратов/инструментов. Повтор с тем же ключом возвращает сохранённый результат (не создаёт дубль), тот же ключ с другим телом — 422 idempotency_key_reuse |
| Route Target | Маршрут каскада: MID + приоритет + фильтры | Куда PA направит платёж (какой PSP/MID, в каком порядке). Настраивается Оператором; платёж без подходящего маршрута — 422 no_eligible_route |
| connector | Коннектор PSP внутри PA | Программная интеграция PA с конкретным PSP (apcopay, payadmit). Определяет supported-возможности (3DS, capture-методы, платёжные данные) — capabilities маршрута |
POST /v1/payments;Как казино может принимать карточные депозиты через PA, чем модели отличаются и что каждая значит для вашего PCI DSS scope. Ключи и шаги интеграции — Handover Package; детали SDK — SDK + Webhooks и SDK README.
Рекомендованная модель: iframe-виджет на странице депозита.
Как это работает:
<script src="https://<pa-origin>/pa-sdk/secure-fields.js"></script>
<div id="pa-card"></div>
<script>
const fields = PASecureFields.mount({
container: '#pa-card',
frameUrl: 'https://<pa-origin>/pa-sdk/secure-fields-frame.html',
captureUrl: '<выдаёт Оператор>',
publishableKey: 'pk_...', // публичный ключ мерчанта
onToken: (r) => {
// r.vaultToken — single-use vault token → сразу на СВОЙ backend
fetch('/my/api/deposit', { method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ vault_token: r.vaultToken, amount_cents: 1500 }) });
},
onError: (e) => showError(e.code, e.message),
});
</script>
Минимальный рабочий пример (включая локальный smoke) — SDK README.
PAN/CVV игрок вводит внутри iframe, размещённого на origin PA; значения полей недоступны JS казино. Данные карты уходят напрямую в capture-поверхность PA — мимо вашего backend.
Ваш backend получает только vault_token, обменивает его на
pin_…-инструмент (POST /v1/payment-instruments) и создаёт платёж
(POST /v1/payments).
PCI DSS следствие: казино ВНЕ PCI DSS scope. PAN/CVV ни на секунду не попадают в системы казино (ни в DOM, ни в backend, ни в логи) — ваша среда не хранит и не обрабатывает карточные данные, и самооценка по минимальной форме остаётся тривиальной. Это главный аргумент, почему SDK — рекомендованный путь.
Модель поддерживает и сохранённые карты: одна регистрация инструмента = один платёж в MVP (single-use), повторный депозит — новый vault token.
Redirect-модель «как у PayAdmit HPP»: игрок перенаправляется на хостед-страницу оплаты на стороне PA/PSP, вводит карту там и возвращается на сайт казино.
next_action: {type: "redirect", redirect_url, method} — вы переводите игрока по redirect_url,
финал приходит webhook'ом и/или GET /v1/payments/{id} (шаг К5
чек-листа).next_action используется для 3DS challenge и hosted page PSP, а не для
полностраничной оплаты на стороне PA.Модель «полный server-to-server»: ваш backend принимает номер карты и CVV игрока и пересылает их в платёжный API.
PA сознательно не предоставляет такой путь. Причины — честно:
instrument_id, полученному от vault token. Ввести
PAN через backend казино контрактно невозможно — это защита обеих сторон,
а не ограничение-заглушка.Если требуется собрать карту без SDK iframe — это делается на стороне PA (hosted page, пункт (b) выше), а не на стороне казино.
| Модель | Статус | Кто видит PAN/CVV | PCI DSS scope казино |
|---|---|---|---|
| Secure Fields SDK (iframe) | доступна | iframe на origin PA | вне scope |
| Hosted payment page | Phase 2 | страница PA/PSP | вне scope |
| S2S raw card | не предоставляется | backend казино | SAQ D / A-EP — полный scope |
next_action;Как платформа PA устроит выплату выигрышей (payout) и почему endpoints выплат сейчас не входят в merchant-view документации. Приём депозитов — Handover Package; глоссарий — Термінологія.
Payout-поверхность не входит в текущий merchant-релиз: endpoints
/v1/payouts отсутствуют в merchant-view справочнике /docs и не
гарантируются для мерчант-интеграции. В контракте платформы
(GET /docs/openapi.json) схема payout-домена уже зафиксирована — это
намеренная упреждающая публикация контракта, а не рабочая поверхность Phase 1.
Payout в PA — это отдельный домен с собственным жизненным циклом, а не «отрицательный платёж»:
instrument_id, masked receiver metadata — PAN/CVV-полезная нагрузка
отклоняется). Запрос идемпотентен — обязателен Idempotency-Key.AWAITING_APPROVAL и ждёт решения оператора платформы
(approve / decline / cancel). Это fail-closed модель: выплата без
одобрения не уйдёт в PSP.next_action платежного/пayout-контракта маркирует это как
{type: "approval", resource, allowed_actions} — внешнему наблюдателю
видно, что ресурс ждёт операторского решения.Idempotency-Key реплеит сохранённое решение.PROCESSING → COMPLETED (или FAILED_TECHNICAL / DECLINED).payouts_enabled: пока флаг выключен, каждый payout-endpoint отвечает
404 feature_disabled. Включение флага = включение домена на стенде.POST /v1/payouts
(идемпотентно, Idempotency-Key);GET /v1/payouts/{id} и список с фильтрами;AWAITING_APPROVAL →
PROCESSING → COMPLETED/DECLINED) — после включения домена;Интегрировать выплаты до объявления Phase 2 не следует: контракт может дозреть (имена полей останутся, гарантии готовности — нет).
next_action.Как платформа PA устроит выплату выигрышей (payout) и почему endpoints выплат сейчас не входят в merchant-view документации. Приём депозитов — Handover Package; глоссарий — Термінологія.
Payout-поверхность не входит в текущий merchant-релиз: endpoints
/v1/payouts отсутствуют в merchant-view справочнике /docs и не
гарантируются для мерчант-интеграции. В контракте платформы
(GET /docs/openapi.json) схема payout-домена уже зафиксирована — это
намеренная упреждающая публикация контракта, а не рабочая поверхность Phase 1.
Payout в PA — это отдельный домен с собственным жизненным циклом, а не «отрицательный платёж»:
instrument_id, masked receiver metadata — PAN/CVV-полезная нагрузка
отклоняется). Запрос идемпотентен — обязателен Idempotency-Key.AWAITING_APPROVAL и ждёт решения оператора платформы
(approve / decline / cancel). Это fail-closed модель: выплата без
одобрения не уйдёт в PSP.next_action платежного/пayout-контракта маркирует это как
{type: "approval", resource, allowed_actions} — внешнему наблюдателю
видно, что ресурс ждёт операторского решения.Idempotency-Key реплеит сохранённое решение.PROCESSING → COMPLETED (или FAILED_TECHNICAL / DECLINED).payouts_enabled: пока флаг выключен, каждый payout-endpoint отвечает
404 feature_disabled. Включение флага = включение домена на стенде.POST /v1/payouts
(идемпотентно, Idempotency-Key);GET /v1/payouts/{id} и список с фильтрами;AWAITING_APPROVAL →
PROCESSING → COMPLETED/DECLINED) — после включения домена;Интегрировать выплаты до объявления Phase 2 не следует: контракт может дозреть (имена полей останутся, гарантии готовности — нет).
next_action.Контракт токенизации карты и платёжных инструментов: Secure Fields SDK
(browser-capture на реальном Locker реализован — POST /v1/sdk/capture,
publishable key — см. §4). Связанные гайды: Field Reference
(поля платежа), Integration models (модели интеграции
и PCI scope), SDK README (быстрый старт), Термінологія.
┌──────────────────────────── казино (frontend) ───────────────────────────┐
│ страница депозита │
│ └── PA Secure Fields SDK → <iframe sandbox> (origin PA) │
│ └── PAN/CVV: ввод и ОТПРАВКА НАПРЯМУЮ в capture-поверхность │
│ POST /v1/sdk/capture — МИНУЯ casino backend │
└───────────────┬──────────────────────────────────────────────────────────┘
│ TLS; PA — транзит (без записи PAN/CVV) → fork → Locker (JWE+JWS)
│ только single-use vault token (postMessage)
▼
casino backend ── POST /v1/payment-instruments {vault_token} ──▶ PA API
│ │
│ PA ── masked meta ─┘
│ (brand/last4 от vault)
└── POST /v1/payments {payment_instrument_id} ──▶ PA API
│
cascade engine │
casino backend ◀── подписанный webhook (HMAC-SHA256) ────┘
Инварианты (проверяются тестами payment_instrument_e2e::casino_smoke_end_to_end_with_pci_hygiene):
****<last4>.tenant + merchant: cross-merchant переиспользование запрещено
(422 instrument_merchant_mismatch).(tenant_id, vault_token_ref)), инструмент потребляется первым платежом атомарно с
созданием платежа (одна транзакция: crash не может оставить «потраченный» инструмент без
платежа).Аутентификация — та же, что для всех операций API: Authorization: Bearer <tenant_api_key>;
rate limiting применяется (route pattern /v1/payment-instruments).
Заголовок Idempotency-Key обязателен (контракт идемпотентности — pa-payments-api.md §3).
{
"merchant_id": "mer_...", // скоуп тенанта
"vault_token": "pm_...", // single-use vault token от capture-поверхности
"customer_reference": "usr_123" // опционально, игрок казино
}
Правила обработки: схема запроса не содержит карточных полей — PAN/CVV физически не могут быть приняты (неизвестные поля игнорируются парсером и не сохраняются). PA:
unknown_merchant);vault_retrieve_card несёт vault_token_fingerprint, не токен);PaymentInstrument (PA token pin_...) + audit event create_payment_instrument
в одной транзакции (details: merchant_id, brand, exp_month/year, customer_reference,
vault_token_fingerprint — БЕЗ токена и карточных данных);customer_reference: опциональное поле — свободный текст, в котором
карточные данные могли бы попасть в записи PA, поэтому его форма ограничена (общий валидатор PA;
тот же применяется на /v1/payouts для customer_reference и в /v1/payments для
customer.reference_id): длина ≤ 255 символов; opaque-charset [A-Za-z0-9_.@-] (без пробелов,
/?# и прочей пунктуации); Luhn-валидная 13–19-значная последовательность (PAN-проба) отвергается.
PAN-проба применяется и к форматированным PAN: разделители [._@-] между группами
цифр снимаются, и Luhn-валидная 13–19-значная последовательность в «схлопнутом» значении
отвергается (форматированный PAN = PAN по PCI DSS; легитимные референсы с сепараторами —
INV-2024-001 — проходят). customer.reference_id в /v1/payments валидируется независимо от
JSON-типа: строка — как есть; JSON-число — в канонической десятичной записи тем же
валидатором (PAN цифрами как число → 422); null — отсутствие опционального поля; прочие
типы → 422 invalid_request (customer.reference_id must be a string).
Нарушение → 422 invalid_request; сообщение называет ограничение, но не эхоит значение.
Проверка выполняется ДО вызова vault и до создания idempotency-ключа.
Ответ 201 Created (masked view):
{
"instrument_id": "pin_...",
"merchant_id": "mer_...",
"status": "ACTIVE",
"card_meta": {"masked_pan": "****4242", "brand": "VISA", "exp_month": "08", "exp_year": "2028"},
"customer_reference": "usr_123",
"created_at": "2026-09-20T12:00:00Z"
}
Коды ошибок:
| Код | HTTP | Причина | Класс |
|---|---|---|---|
invalid_request |
400/422 | пустой/длинный (>255) vault_token, недопустимый charset (только [A-Za-z0-9_.:-] — без /?# и пробелов), кривой merchant_id; нарушения контракта customer_reference |
детерминированный |
idempotency_key_required |
400 | нет Idempotency-Key |
детерминированный |
idempotency_key_reuse |
422 | тот же ключ с другим телом | детерминированный |
idempotency_in_flight |
409 | оригинал ещё выполняется (Retry-After) |
— |
unknown_merchant |
422 | merchant не в тенанте | детерминированный |
vault_token_unknown |
422 | capture-поверхность не знает токен (потрачен/чужой) | детерминированный |
vault_unavailable |
502 | vault недоступен/отверг ключ/невалидный ответ (детали — только в логах PA) | транзиентный |
vault_token_already_registered |
409 | токен уже зарегистрирован другим инструментом | детерминированный |
Идемпотентность классов: детерминированные коды сохраняются в
idempotency-записи и реплеятся тем же ключом (повтор возвращает сохранённый результат). Транзиентный
vault_unavailable НЕ сохраняется: claim ключа освобождается, и повтор тем же ключом начинает
новую попытку доставки (новый вызов vault → возможен 201), а не реплей запиненной 502 на TTL.
Тот же проекция, что в 201; статус отражает жизненный цикл (ACTIVE/USED/DEACTIVATED).
Чужой/несуществующий id — одинаковый 404 instrument_not_found (без утечки существования).
vault_token_ref в проекции отсутствует на уровне структуры (unit-тест фиксирует отсутствие
поля в сериализации).
POST /v1/payments принимает payment_method.instrument_id (= pin_...):
{
"merchant_id": "mer_...",
"amount": 1500,
"currency": "EUR",
"payment_method": {"type": "card", "instrument_id": "pin_..."},
"capture_method": "automatic"
}
Валидация (до создания Payment, в транзакции idempotency-ключа):
| Код | HTTP | Причина |
|---|---|---|
unknown_instrument |
422 | нет такого pin_ в тенанте |
instrument_merchant_mismatch |
422 | инструмент другого merchant'а тенанта |
instrument_not_active |
422 | уже использован (USED) или деактивирован |
При успехе: инструмент атомарно переводится ACTIVE → USED той же транзакцией, что создаёт
Payment (single-use), instrument_id фиксируется на Payment/Attempt-журнале; дальше —
обычный cascade.
POST /v1/merchants/{id}/webhook {"url": "https://casino.example/pa/webhook"} —
регистрирует endpoint (http разрешён для dev; prod — HTTPS, origin allowlist — POST_MVP).
Ответ содержит webhook_secret ровно один раз (whs_...); аудит — set_merchant_webhook
(секрет в details не пишется); GET /v1/merchants/{id} возвращает только webhook_url.
Доставка: когда платёж этого merchant'а достигает финального статуса, PA
записывает одно событие в транзакционный outbox webhook_deliveries — В ТОЙ ЖЕ
транзакции, что и доменное изменение статуса. Доставку выполняет асинхронный
worker (crate::webhook_worker): poll PENDING с наступившим next_retry_at → POST
сохранённого payload'а → DELIVERED (2xx) | retry: attempts++,
next_retry_at = now + backoff (лестница 1м/5м/30м/2h) → после 5 неудачных попыток DEAD
(dead-letter, больше не доставляется). Запрос create_payment больше НЕ блокируется
доставкой и не может «упасть» из-за получателя:
{
"event_id": "0197...", // UUIDv7 — ключ дедупликации
"event_type": "payment.authorized",
"created_at": "2026-09-20T12:00:01Z",
"data": {
"payment_id": "pay_...", "merchant_id": "mer_...", "status": "AUTHORIZED",
"amount": 1500, "currency": "EUR", "instrument_id": "pin_...",
"correlation_id": "...", "attempts_summary": [...]
}
}
Заголовки: PA-Webhook-Signature: v1=<hex(HMAC-SHA256(raw_body, webhook_secret))>,
PA-Event-Id: <event_id>. Типы: payment.authorized, payment.captured, payment.refunded,
payment.failed (а также async-переходы sync worker'а: UNKNOWN → AUTHORIZED|FAILED —
ARC-PA6-11). PENDING/UNKNOWN_PENDING_SYNC не доставляются.
Свойства доставки (PG-7):
event_id); каждая
попытка (в т.ч. retry) переотправляет ТЕ ЖЕ байты payload'а → одна и та же подпись
(при неизменном секрете); получатель дедуплицирует по event_id;(payment_id, event_type, data.status) (миграция 0018). Повторная
эмиссия того же факта (повторный sync-тик, неудачный refund, не изменивший
статус платежа, дублирующая запись состояния) под НОВЫМ event_id — no-op:
казино видит каждый терминальный факт РОВНО ОДИН РАЗ, дедуп получателя по
PA-Event-Id не обязан ловить новые id. Разные переходы одного платежа
(AUTHORIZED → PARTIALLY_REFUNDED → REFUNDED) несут свои события;
retry-доставки не затронуты (at-least-once по event_id сохраняется);webhook_secret берётся из конфигурации merchant'а в
момент доставки;status=DEAD,
last_error хранит класс последней ошибки (status:<code>/timeout/connect);GET /v1/webhooks/deliveries?payment_id=pay_... (Bearer ключ
tenant'а, tenant-scoped — чужой платёж = 404): статус, attempts, last_error,
created_at, delivered_at по каждому событию платежа.Ошибка доставки НЕ влияет на результат платежа (только лог без тела события).
POST /v1/merchants/{id}/rotate-webhook-secret — ротация подписывающего секрета безостановки доставки. Тело не требуется (секрет генерирует PA).
whs_...) генерируется сервер-сайд и возвращается ровно один раз
(webhook_secret), как при регистрации; он становится активным немедленно — все
доставки подписываются уже НОВЫМ секретом.merchants.webhook_secret_prev +
webhook_secret_rotated_at (миграция 0013). В течение grace-окна
(PA_WEBHOOK_SECRET_GRACE_DAYS, по умолчанию 7 дней; окно отсчитывается от
webhook_secret_rotated_at) получатель принимает оба секрета (dual-verify) —
схема rollout'а на стороне казино: обновить верификацию → ротация → убедиться, что
старые проверки больше не приходят.{merchant_id, webhook_url, webhook_secret, old_secret_expires_at, rotated_at};
old_secret_expires_at = rotated_at + grace — момент, после которого старый секрет
обязан отвергаться. Эталонная реализация dual-verify —
pa_api::webhook::verify_event_signature(secret, secret_prev, rotated_at, grace, body, header, now):
активный секрет валиден всегда, предыдущий — только пока now <= rotated_at + grace.POST /v1/merchants/{id}/webhook) сдвигает секрет
по тому же инварианту (предыдущий → grace-слот); вторая ротация выталкивает
предыдущий-предыдущий секрет из окна — в grace живёт ровно один секрет.rotate_webhook_secret в той же транзакции, что и изменение; details
содержат webhook_url, rotated_at, grace_days — секреты в details и логах не
пишутся никогда.404 merchant_not_found (чужой/неизвестный мерчант — tenant-scoping),
409 webhook_not_registered (нет зарегистрированного endpoint'а — сначала
POST /v1/merchants/{id}/webhook), 401 без Bearer-ключа.Drop-in, без сборки: pa-sdk/secure-fields.js + pa-sdk/secure-fields-frame.html.
const fields = PASecureFields.mount({
container: '#pa-card',
frameUrl: 'https://<host>/secure-fields-frame.html', // казино хостит статику
captureUrl: 'https://<pa-origin>/v1/sdk/capture', // реальный capture
publishableKey: 'pk_<merchant-uuid>_<digest>', // PUBLIC, из онбординга мерчанта
onToken: (r) => { /* r.vaultToken, r.brand, r.last4 */ },
onError: (e) => { /* e.code, e.message */ },
onReady: => {},
});
fields.destroy ; fields.isReady ; fields.token ;
Контракт безопасности:
sandbox="allow-scripts allow-forms" (БЕЗ allow-same-origin) —
фрейм имеет opaque origin, не имеет доступа к DOM/storage родителя, родитель не читает
DOM фрейма: casino JS не имеет DOM-доступа к значениям полей (security invariant из
pa-payments-api.md §2.0).pa-secure-fields;
токен валидируется по форме до вызова onToken.captureUrl (fetch, X-Publishable-Key +
flat-JSON card_number/cvc/expiry_month/expiry_year/card_holder_name) — единственный
сетевой вызов с карточными данными; в demo-режиме (captureUrl: 'demo') токен mintится
внутри фрейма без сети (PAN остаётся и умирает в iframe; publishable key не нужен).[A-Za-z0-9_-]{8,255}
(dev-схема vtok_<brand>_<last4>-<uuid>, capture-поверхность — pm_...).invalid_publishable_key, origin_not_allowed,
invalid_card, rate_limited, capture_unavailable (маппинг статусов endpoint'а —
§2).onToken возвращает vaultToken — его казино-backend отправляет в
POST /v1/payment-instruments (§2.1). Схема интеграции казино (backend + frontend) —
pa-sdk/README.md.
| Режим | captureUrl |
Что происходит |
|---|---|---|
demo |
demo |
Тестовый double: токен vtok_... mintится в iframe локально (CI/E2E/demo без стенда). Никакой передачи карточных данных. |
| **fork через PA ** | https://<pa-origin>/v1/sdk/capture |
iframe шлёт PAN/CVV напрямую на capture-endpoint PA → fork POST /payment_methods (JWE→Locker) → pm_... + маскированные метаданные. PA — только транзит: без записи PAN/CVV в DB/логи/аудит (§1). Auth: publishable key мерчанта (X-Publishable-Key, PUBLIC — не секрет); жёсткий per-key rate limit (PA_SDK_CAPTURE_RPM, 30/мин) + per-IP окно; origin allowlist (PA_SDK_CAPTURE_ALLOWED_ORIGINS) опционально; HTTPS-only на публичных адресах в PA_ENV=production (fail-fast). |
| Locker напрямую (post-MVP) | Locker hosted fields | PAN/CVV → Locker (basilisk add_card, JWE+JWS) → card_reference. Требует browser-JWE capture-страницы от Locker. |
Publishable key: derived, без хранения —
pk_<merchant-uuid>_<hex(SHA-256(pepper ‖ uuid))[..8]>; выдаётся в ответе
POST /v1/merchants и в GET /v1/merchants/{id} (детерминированный →
повторно читаемый, ротация = смена PA_PUBLISHABLE_KEY_PEPPER). Не секрет: привязывает
capture к мерчанту (и к его rate-limit-бюджету), прав не даёт — деньги двигает только
tenant Bearer. Pepper не задан → digest от пустого pepper (публичная well-formed
привязка; честно задокументировано в sdk-hardening.md §3).
Конфигурация PA (env):
| Переменная | Значение |
|---|---|
PA_ENV |
окружение развёртывания: dev (по умолчанию) | production (prod). В production включены fail-fast инварианты: PA_CARD_VAULT_MODE=mock запрещён; plaintext-http:// fork URL на публичный адрес запрещён; неизвестное значение — ошибка конфигурации |
PA_CARD_VAULT_MODE |
mock (по умолчанию; dev-схема vtok_) | fork (capture-поверхность стенда: и resolve метаданных, и browser-capture через PA) |
PA_HS_FORK_BASE_URL |
базовый URL capture-поверхности (fork mode; маршруты без /v1 — особенность сборки стенда) |
PA_HS_FORK_API_KEY |
API-ключ capture-поверхности (секрет; только env, не логируется) |
PA_HS_FORK_CUSTOMER_ID |
fork-customer capture-поверхности (опционально; locker-live-smoke.md §2) |
PA_PUBLISHABLE_KEY_PEPPER |
pepper derived publishable-ключей (секрет, env-only) |
PA_SDK_CAPTURE_RPM |
жёсткий лимит capture на publishable key (по умолчанию 30/мин) |
PA_SDK_CAPTURE_ALLOWED_ORIGINS |
origin allowlist браузерного контура (пусто → wildcard-CORS, MVP-постулат) |
PA_WEBHOOK_TIMEOUT_SECS |
таймаут доставки webhook, 1..30 (по умолчанию 5) |
PA_WEBHOOK_WORKER_ENABLED |
webhook delivery worker, по умолчанию on |
PA_WEBHOOK_WORKER_INTERVAL_SECS |
интервал тика delivery worker, по умолчанию 5 |
PA_WEBHOOK_WORKER_BATCH |
макс. строк outbox за тик, по умолчанию 50 |
PA_WEBHOOK_MAX_ATTEMPTS |
бюджет попыток на событие, по умолчанию 5 (после — DEAD) |
PA_WEBHOOK_BACKOFF_SECS |
лестница backoff в секундах, по умолчанию 60,300,1800,7200 (1м/5м/30м/2h) |
PA_WEBHOOK_CLAIM_SECS |
окно claim'а строки worker'ом, по умолчанию 60 |
PA_WEBHOOK_WORKER_SCOPE |
merchant-scope worker'а: all или список uuid |
В fork mode при отсутствии кредов PA падает на старте (fail-fast) — инструментальная поверхность без рабочего vault'а отвечала бы 502 на каждую регистрацию.
Fail-fast перед pilot: при PA_ENV=production конфигурация
PA_CARD_VAULT_MODE=mock отвергается на старте (config error, exit 2) — mock-vault резолвит
любые self-describing vtok_ токены и отвечал бы 201 на карты, которых не существует ни в одном
vault; производственное развёртывание обязано использовать fork (или будущий Locker-режим).
добавляет: в production plaintext-http:// PA_HS_FORK_BASE_URL на публичный
IP/hostname отвергается на старте — браузерный capture-трафик (PAN/CVV) обязан идти по TLS
или по network-isolated адресу (loopback/RFC1918/docker-hostname).
POST /v1/sdk/capture (capture-proxy на origin PA →
fork Locker), publishable key, жёсткий rate limit, CORS/origin allowlist,
HTTPS-only fail-fast —.pa_pk_...) + origin allowlist для браузерного контураpk_<merchant-uuid>_<digest>, derived без хранения;
origin allowlist — PA_SDK_CAPTURE_ALLOWED_ORIGINS, по умолчанию wildcard-CORS —
honest MVP-posture, см. sdk-hardening.md §8).single_use=false), provider_tokens, verify/deactivate
endpoints, EXPIRED/PURGED lifecycle (domain-model §6) — после PG-2/PCI review.
проверил и зафиксировал статус: инструмент в MVP одноразовый, сам vault token pm_... в Locker многоразовый; saved-card UX —
follow-up (sdk-hardening.md §6).webhook_deliveries, delivery worker с retry/backoff (1м/5м/30м/2h), dead-letter
после 5 попыток, дедуп по event_id, журнал GET /v1/webhooks/deliveries.Контракт токенизации карты и платёжных инструментов: Secure Fields SDK
(browser-capture на реальном Locker реализован — POST /v1/sdk/capture,
publishable key — см. §4). Связанные гайды: Field Reference
(поля платежа), Integration models (модели интеграции
и PCI scope), SDK README (быстрый старт), Термінологія.
┌──────────────────────────── казино (frontend) ───────────────────────────┐
│ страница депозита │
│ └── PA Secure Fields SDK → <iframe sandbox> (origin PA) │
│ └── PAN/CVV: ввод и ОТПРАВКА НАПРЯМУЮ в capture-поверхность │
│ POST /v1/sdk/capture — МИНУЯ casino backend │
└───────────────┬──────────────────────────────────────────────────────────┘
│ TLS; PA — транзит (без записи PAN/CVV) → fork → Locker (JWE+JWS)
│ только single-use vault token (postMessage)
▼
casino backend ── POST /v1/payment-instruments {vault_token} ──▶ PA API
│ │
│ PA ── masked meta ─┘
│ (brand/last4 от vault)
└── POST /v1/payments {payment_instrument_id} ──▶ PA API
│
cascade engine │
casino backend ◀── подписанный webhook (HMAC-SHA256) ────┘
Инварианты (проверяются тестами payment_instrument_e2e::casino_smoke_end_to_end_with_pci_hygiene):
****<last4>.tenant + merchant: cross-merchant переиспользование запрещено
(422 instrument_merchant_mismatch).(tenant_id, vault_token_ref)), инструмент потребляется первым платежом атомарно с
созданием платежа (одна транзакция: crash не может оставить «потраченный» инструмент без
платежа).Аутентификация — та же, что для всех операций API: Authorization: Bearer <tenant_api_key>;
rate limiting применяется (route pattern /v1/payment-instruments).
Заголовок Idempotency-Key обязателен (контракт идемпотентности — pa-payments-api.md §3).
{
"merchant_id": "mer_...", // скоуп тенанта
"vault_token": "pm_...", // single-use vault token от capture-поверхности
"customer_reference": "usr_123" // опционально, игрок казино
}
Правила обработки: схема запроса не содержит карточных полей — PAN/CVV физически не могут быть приняты (неизвестные поля игнорируются парсером и не сохраняются). PA:
unknown_merchant);vault_retrieve_card несёт vault_token_fingerprint, не токен);PaymentInstrument (PA token pin_...) + audit event create_payment_instrument
в одной транзакции (details: merchant_id, brand, exp_month/year, customer_reference,
vault_token_fingerprint — БЕЗ токена и карточных данных);customer_reference: опциональное поле — свободный текст, в котором
карточные данные могли бы попасть в записи PA, поэтому его форма ограничена (общий валидатор PA;
тот же применяется на /v1/payouts для customer_reference и в /v1/payments для
customer.reference_id): длина ≤ 255 символов; opaque-charset [A-Za-z0-9_.@-] (без пробелов,
/?# и прочей пунктуации); Luhn-валидная 13–19-значная последовательность (PAN-проба) отвергается.
PAN-проба применяется и к форматированным PAN: разделители [._@-] между группами
цифр снимаются, и Luhn-валидная 13–19-значная последовательность в «схлопнутом» значении
отвергается (форматированный PAN = PAN по PCI DSS; легитимные референсы с сепараторами —
INV-2024-001 — проходят). customer.reference_id в /v1/payments валидируется независимо от
JSON-типа: строка — как есть; JSON-число — в канонической десятичной записи тем же
валидатором (PAN цифрами как число → 422); null — отсутствие опционального поля; прочие
типы → 422 invalid_request (customer.reference_id must be a string).
Нарушение → 422 invalid_request; сообщение называет ограничение, но не эхоит значение.
Проверка выполняется ДО вызова vault и до создания idempotency-ключа.
Ответ 201 Created (masked view):
{
"instrument_id": "pin_...",
"merchant_id": "mer_...",
"status": "ACTIVE",
"card_meta": {"masked_pan": "****4242", "brand": "VISA", "exp_month": "08", "exp_year": "2028"},
"customer_reference": "usr_123",
"created_at": "2026-09-20T12:00:00Z"
}
Коды ошибок:
| Код | HTTP | Причина | Класс |
|---|---|---|---|
invalid_request |
400/422 | пустой/длинный (>255) vault_token, недопустимый charset (только [A-Za-z0-9_.:-] — без /?# и пробелов), кривой merchant_id; нарушения контракта customer_reference |
детерминированный |
idempotency_key_required |
400 | нет Idempotency-Key |
детерминированный |
idempotency_key_reuse |
422 | тот же ключ с другим телом | детерминированный |
idempotency_in_flight |
409 | оригинал ещё выполняется (Retry-After) |
— |
unknown_merchant |
422 | merchant не в тенанте | детерминированный |
vault_token_unknown |
422 | capture-поверхность не знает токен (потрачен/чужой) | детерминированный |
vault_unavailable |
502 | vault недоступен/отверг ключ/невалидный ответ (детали — только в логах PA) | транзиентный |
vault_token_already_registered |
409 | токен уже зарегистрирован другим инструментом | детерминированный |
Идемпотентность классов: детерминированные коды сохраняются в
idempotency-записи и реплеятся тем же ключом (повтор возвращает сохранённый результат). Транзиентный
vault_unavailable НЕ сохраняется: claim ключа освобождается, и повтор тем же ключом начинает
новую попытку доставки (новый вызов vault → возможен 201), а не реплей запиненной 502 на TTL.
Тот же проекция, что в 201; статус отражает жизненный цикл (ACTIVE/USED/DEACTIVATED).
Чужой/несуществующий id — одинаковый 404 instrument_not_found (без утечки существования).
vault_token_ref в проекции отсутствует на уровне структуры (unit-тест фиксирует отсутствие
поля в сериализации).
POST /v1/payments принимает payment_method.instrument_id (= pin_...):
{
"merchant_id": "mer_...",
"amount": 1500,
"currency": "EUR",
"payment_method": {"type": "card", "instrument_id": "pin_..."},
"capture_method": "automatic"
}
Валидация (до создания Payment, в транзакции idempotency-ключа):
| Код | HTTP | Причина |
|---|---|---|
unknown_instrument |
422 | нет такого pin_ в тенанте |
instrument_merchant_mismatch |
422 | инструмент другого merchant'а тенанта |
instrument_not_active |
422 | уже использован (USED) или деактивирован |
При успехе: инструмент атомарно переводится ACTIVE → USED той же транзакцией, что создаёт
Payment (single-use), instrument_id фиксируется на Payment/Attempt-журнале; дальше —
обычный cascade.
POST /v1/merchants/{id}/webhook {"url": "https://casino.example/pa/webhook"} —
регистрирует endpoint (http разрешён для dev; prod — HTTPS, origin allowlist — POST_MVP).
Ответ содержит webhook_secret ровно один раз (whs_...); аудит — set_merchant_webhook
(секрет в details не пишется); GET /v1/merchants/{id} возвращает только webhook_url.
Доставка: когда платёж этого merchant'а достигает финального статуса, PA
записывает одно событие в транзакционный outbox webhook_deliveries — В ТОЙ ЖЕ
транзакции, что и доменное изменение статуса. Доставку выполняет асинхронный
worker (crate::webhook_worker): poll PENDING с наступившим next_retry_at → POST
сохранённого payload'а → DELIVERED (2xx) | retry: attempts++,
next_retry_at = now + backoff (лестница 1м/5м/30м/2h) → после 5 неудачных попыток DEAD
(dead-letter, больше не доставляется). Запрос create_payment больше НЕ блокируется
доставкой и не может «упасть» из-за получателя:
{
"event_id": "0197...", // UUIDv7 — ключ дедупликации
"event_type": "payment.authorized",
"created_at": "2026-09-20T12:00:01Z",
"data": {
"payment_id": "pay_...", "merchant_id": "mer_...", "status": "AUTHORIZED",
"amount": 1500, "currency": "EUR", "instrument_id": "pin_...",
"correlation_id": "...", "attempts_summary": [...]
}
}
Заголовки: PA-Webhook-Signature: v1=<hex(HMAC-SHA256(raw_body, webhook_secret))>,
PA-Event-Id: <event_id>. Типы: payment.authorized, payment.captured, payment.refunded,
payment.failed (а также async-переходы sync worker'а: UNKNOWN → AUTHORIZED|FAILED —
ARC-PA6-11). PENDING/UNKNOWN_PENDING_SYNC не доставляются.
Свойства доставки (PG-7):
event_id); каждая
попытка (в т.ч. retry) переотправляет ТЕ ЖЕ байты payload'а → одна и та же подпись
(при неизменном секрете); получатель дедуплицирует по event_id;(payment_id, event_type, data.status) (миграция 0018). Повторная
эмиссия того же факта (повторный sync-тик, неудачный refund, не изменивший
статус платежа, дублирующая запись состояния) под НОВЫМ event_id — no-op:
казино видит каждый терминальный факт РОВНО ОДИН РАЗ, дедуп получателя по
PA-Event-Id не обязан ловить новые id. Разные переходы одного платежа
(AUTHORIZED → PARTIALLY_REFUNDED → REFUNDED) несут свои события;
retry-доставки не затронуты (at-least-once по event_id сохраняется);webhook_secret берётся из конфигурации merchant'а в
момент доставки;status=DEAD,
last_error хранит класс последней ошибки (status:<code>/timeout/connect);GET /v1/webhooks/deliveries?payment_id=pay_... (Bearer ключ
tenant'а, tenant-scoped — чужой платёж = 404): статус, attempts, last_error,
created_at, delivered_at по каждому событию платежа.Ошибка доставки НЕ влияет на результат платежа (только лог без тела события).
POST /v1/merchants/{id}/rotate-webhook-secret — ротация подписывающего секрета безостановки доставки. Тело не требуется (секрет генерирует PA).
whs_...) генерируется сервер-сайд и возвращается ровно один раз
(webhook_secret), как при регистрации; он становится активным немедленно — все
доставки подписываются уже НОВЫМ секретом.merchants.webhook_secret_prev +
webhook_secret_rotated_at (миграция 0013). В течение grace-окна
(PA_WEBHOOK_SECRET_GRACE_DAYS, по умолчанию 7 дней; окно отсчитывается от
webhook_secret_rotated_at) получатель принимает оба секрета (dual-verify) —
схема rollout'а на стороне казино: обновить верификацию → ротация → убедиться, что
старые проверки больше не приходят.{merchant_id, webhook_url, webhook_secret, old_secret_expires_at, rotated_at};
old_secret_expires_at = rotated_at + grace — момент, после которого старый секрет
обязан отвергаться. Эталонная реализация dual-verify —
pa_api::webhook::verify_event_signature(secret, secret_prev, rotated_at, grace, body, header, now):
активный секрет валиден всегда, предыдущий — только пока now <= rotated_at + grace.POST /v1/merchants/{id}/webhook) сдвигает секрет
по тому же инварианту (предыдущий → grace-слот); вторая ротация выталкивает
предыдущий-предыдущий секрет из окна — в grace живёт ровно один секрет.rotate_webhook_secret в той же транзакции, что и изменение; details
содержат webhook_url, rotated_at, grace_days — секреты в details и логах не
пишутся никогда.404 merchant_not_found (чужой/неизвестный мерчант — tenant-scoping),
409 webhook_not_registered (нет зарегистрированного endpoint'а — сначала
POST /v1/merchants/{id}/webhook), 401 без Bearer-ключа.Drop-in, без сборки: pa-sdk/secure-fields.js + pa-sdk/secure-fields-frame.html.
const fields = PASecureFields.mount({
container: '#pa-card',
frameUrl: 'https://<host>/secure-fields-frame.html', // казино хостит статику
captureUrl: 'https://<pa-origin>/v1/sdk/capture', // реальный capture
publishableKey: 'pk_<merchant-uuid>_<digest>', // PUBLIC, из онбординга мерчанта
onToken: (r) => { /* r.vaultToken, r.brand, r.last4 */ },
onError: (e) => { /* e.code, e.message */ },
onReady: => {},
});
fields.destroy ; fields.isReady ; fields.token ;
Контракт безопасности:
sandbox="allow-scripts allow-forms" (БЕЗ allow-same-origin) —
фрейм имеет opaque origin, не имеет доступа к DOM/storage родителя, родитель не читает
DOM фрейма: casino JS не имеет DOM-доступа к значениям полей (security invariant из
pa-payments-api.md §2.0).pa-secure-fields;
токен валидируется по форме до вызова onToken.captureUrl (fetch, X-Publishable-Key +
flat-JSON card_number/cvc/expiry_month/expiry_year/card_holder_name) — единственный
сетевой вызов с карточными данными; в demo-режиме (captureUrl: 'demo') токен mintится
внутри фрейма без сети (PAN остаётся и умирает в iframe; publishable key не нужен).[A-Za-z0-9_-]{8,255}
(dev-схема vtok_<brand>_<last4>-<uuid>, capture-поверхность — pm_...).invalid_publishable_key, origin_not_allowed,
invalid_card, rate_limited, capture_unavailable (маппинг статусов endpoint'а —
§2).onToken возвращает vaultToken — его казино-backend отправляет в
POST /v1/payment-instruments (§2.1). Схема интеграции казино (backend + frontend) —
pa-sdk/README.md.
| Режим | captureUrl |
Что происходит |
|---|---|---|
demo |
demo |
Тестовый double: токен vtok_... mintится в iframe локально (CI/E2E/demo без стенда). Никакой передачи карточных данных. |
| **fork через PA ** | https://<pa-origin>/v1/sdk/capture |
iframe шлёт PAN/CVV напрямую на capture-endpoint PA → fork POST /payment_methods (JWE→Locker) → pm_... + маскированные метаданные. PA — только транзит: без записи PAN/CVV в DB/логи/аудит (§1). Auth: publishable key мерчанта (X-Publishable-Key, PUBLIC — не секрет); жёсткий per-key rate limit (PA_SDK_CAPTURE_RPM, 30/мин) + per-IP окно; origin allowlist (PA_SDK_CAPTURE_ALLOWED_ORIGINS) опционально; HTTPS-only на публичных адресах в PA_ENV=production (fail-fast). |
| Locker напрямую (post-MVP) | Locker hosted fields | PAN/CVV → Locker (basilisk add_card, JWE+JWS) → card_reference. Требует browser-JWE capture-страницы от Locker. |
Publishable key: derived, без хранения —
pk_<merchant-uuid>_<hex(SHA-256(pepper ‖ uuid))[..8]>; выдаётся в ответе
POST /v1/merchants и в GET /v1/merchants/{id} (детерминированный →
повторно читаемый, ротация = смена PA_PUBLISHABLE_KEY_PEPPER). Не секрет: привязывает
capture к мерчанту (и к его rate-limit-бюджету), прав не даёт — деньги двигает только
tenant Bearer. Pepper не задан → digest от пустого pepper (публичная well-formed
привязка; честно задокументировано в sdk-hardening.md §3).
Конфигурация PA (env):
| Переменная | Значение |
|---|---|
PA_ENV |
окружение развёртывания: dev (по умолчанию) | production (prod). В production включены fail-fast инварианты: PA_CARD_VAULT_MODE=mock запрещён; plaintext-http:// fork URL на публичный адрес запрещён; неизвестное значение — ошибка конфигурации |
PA_CARD_VAULT_MODE |
mock (по умолчанию; dev-схема vtok_) | fork (capture-поверхность стенда: и resolve метаданных, и browser-capture через PA) |
PA_HS_FORK_BASE_URL |
базовый URL capture-поверхности (fork mode; маршруты без /v1 — особенность сборки стенда) |
PA_HS_FORK_API_KEY |
API-ключ capture-поверхности (секрет; только env, не логируется) |
PA_HS_FORK_CUSTOMER_ID |
fork-customer capture-поверхности (опционально; locker-live-smoke.md §2) |
PA_PUBLISHABLE_KEY_PEPPER |
pepper derived publishable-ключей (секрет, env-only) |
PA_SDK_CAPTURE_RPM |
жёсткий лимит capture на publishable key (по умолчанию 30/мин) |
PA_SDK_CAPTURE_ALLOWED_ORIGINS |
origin allowlist браузерного контура (пусто → wildcard-CORS, MVP-постулат) |
PA_WEBHOOK_TIMEOUT_SECS |
таймаут доставки webhook, 1..30 (по умолчанию 5) |
PA_WEBHOOK_WORKER_ENABLED |
webhook delivery worker, по умолчанию on |
PA_WEBHOOK_WORKER_INTERVAL_SECS |
интервал тика delivery worker, по умолчанию 5 |
PA_WEBHOOK_WORKER_BATCH |
макс. строк outbox за тик, по умолчанию 50 |
PA_WEBHOOK_MAX_ATTEMPTS |
бюджет попыток на событие, по умолчанию 5 (после — DEAD) |
PA_WEBHOOK_BACKOFF_SECS |
лестница backoff в секундах, по умолчанию 60,300,1800,7200 (1м/5м/30м/2h) |
PA_WEBHOOK_CLAIM_SECS |
окно claim'а строки worker'ом, по умолчанию 60 |
PA_WEBHOOK_WORKER_SCOPE |
merchant-scope worker'а: all или список uuid |
В fork mode при отсутствии кредов PA падает на старте (fail-fast) — инструментальная поверхность без рабочего vault'а отвечала бы 502 на каждую регистрацию.
Fail-fast перед pilot: при PA_ENV=production конфигурация
PA_CARD_VAULT_MODE=mock отвергается на старте (config error, exit 2) — mock-vault резолвит
любые self-describing vtok_ токены и отвечал бы 201 на карты, которых не существует ни в одном
vault; производственное развёртывание обязано использовать fork (или будущий Locker-режим).
добавляет: в production plaintext-http:// PA_HS_FORK_BASE_URL на публичный
IP/hostname отвергается на старте — браузерный capture-трафик (PAN/CVV) обязан идти по TLS
или по network-isolated адресу (loopback/RFC1918/docker-hostname).
POST /v1/sdk/capture (capture-proxy на origin PA →
fork Locker), publishable key, жёсткий rate limit, CORS/origin allowlist,
HTTPS-only fail-fast —.pa_pk_...) + origin allowlist для браузерного контураpk_<merchant-uuid>_<digest>, derived без хранения;
origin allowlist — PA_SDK_CAPTURE_ALLOWED_ORIGINS, по умолчанию wildcard-CORS —
honest MVP-posture, см. sdk-hardening.md §8).single_use=false), provider_tokens, verify/deactivate
endpoints, EXPIRED/PURGED lifecycle (domain-model §6) — после PG-2/PCI review.
проверил и зафиксировал статус: инструмент в MVP одноразовый, сам vault token pm_... в Locker многоразовый; saved-card UX —
follow-up (sdk-hardening.md §6).webhook_deliveries, delivery worker с retry/backoff (1м/5м/30м/2h), dead-letter
после 5 попыток, дедуп по event_id, журнал GET /v1/webhooks/deliveries.Руководство по подключению приёма карточных депозитов через PA. Контракт API — гайд SDK + Webhooks; capture-контур и его security-модель управляются Оператором и мерчанту не требуются.
POST /v1/payment-instruments {vault_token} →
получает pin_... (PA token, маскированные метаданные ****1111, brand).POST /v1/payments {payment_instrument_id, amount, currency} →
cascade → финальный статус.payment.authorized —
зачисляем депозит игроку.PAN/CVV никогда не касаются casino backend и PA: нет ни БД-колонок, ни логов, ни аудита.
| Компонент | Что делает |
|---|---|
pa-sdk/secure-fields.js |
drop-in SDK: рисует iframe-виджет, отдаёт onToken(vaultToken) |
pa-sdk/secure-fields-frame.html |
страница iframe (форма карты + отправка в capture-поверхность). Размещается на origin PA/мерчанта; НЕ меняйте её логику самостоятельно |
| ваш backend | 3 вызова PA API + приём webhook (раздел 3) |
SDK не требует сборки: подключите оба файла статикой (в проде — с origin PA и HTTPS).
Production-пример (реальный capture):
<script src="https://<pa-origin>/pa-sdk/secure-fields.js"></script>
<div id="pa-card"></div>
<script>
const fields = PASecureFields.mount({
container: '#pa-card',
frameUrl: 'https://<pa-origin>/pa-sdk/secure-fields-frame.html',
// Реальная capture-поверхность PA: iframe шлёт PAN/CVV напрямую
// на POST /v1/sdk/capture и получает single-use vault token:
captureUrl: 'https://<pa-origin>/v1/sdk/capture',
// Publishable key мерчанта (PUBLIC — не секрет, вставляется в страницу;
// выдаётся в ответе POST /v1/merchants и GET /v1/merchants/{id}):
publishableKey: 'pk_018f3a2b-...-<16 hex>',
onToken: (r) => {
// r.vaultToken — single-use vault token; отправьте на СВОЙ backend:
fetch('/my/api/deposit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ vault_token: r.vaultToken, amount_cents: 1500 })
});
},
onError: (e) => showError(e.code, e.message),
});
</script>
Коды ошибок capture-шага (onError): invalid_publishable_key (401 — ключ не тот),
origin_not_allowed (403 — origin не в allowlist PA), invalid_card (422 — карта
отклонена), rate_limited (429 — превышен лимит, повторите позже),
capture_unavailable (502 — capture-поверхность недоступна).
Правила:
publishableKey — публичный: он не секрет и не даёт прав, кроме токенизации карт;
храните его прямо во фронтенде, как Stripe pk_;vault_token в URL/логи; это одноразовый credential;vault_token_already_registered);Все вызовы — Authorization: Bearer <API-ключ>: merchant-scoped ключ казино
(mer_sk_… — выдаёт оператор, работает только с вашим merchant_id)
или tenant-ключ оператора. На POST обязателен Idempotency-Key (uuid).
Ошибки — RFC 7807 (code — машинный дискриминатор).
POST /v1/payment-instruments
Idempotency-Key: <uuid>
{
"merchant_id": "mer_...",
"vault_token": "<vaultToken из onToken>",
"customer_reference": "usr_123"
}
201 → {"instrument_id": "pin_...", "card_meta": {"masked_pan": "****1111", "brand": "VISA",...}, "status": "ACTIVE"}.
Сохраните у себя пару (customer_reference, instrument_id).
Коды: vault_token_unknown (токен потрачен/невалиден — запросите у игрока карту заново),
vault_unavailable (capture-поверхность недоступна — повторите с НОВЫМ Idempotency-Key),
vault_token_already_registered (409).
POST /v1/payments
Idempotency-Key: <uuid>
{
"merchant_id": "mer_...",
"amount": 1500, // минорные единицы
"currency": "EUR",
"payment_method": {"type": "card", "instrument_id": "pin_..."},
"capture_method": "automatic"
}
201 → {"payment_id": "pay_...", "status": "AUTHORIZED"|"PENDING"|"FAILED"|..., "attempts_summary": [...]}. Инструмент одноразовый: повторный платёж по тому же
pin_ → 422 instrument_not_active. При PENDING — опрашивайте
GET /v1/payments/{id} (Retry-After); финал — webhook и/или polling.
Однократно зарегистрируйте endpoint (секрет покажется в ответе ровно один раз):
POST /v1/merchants/{id}/webhook
{"url": "https://casino.example/pa/webhook"}
Сменить URL позже можно из портала мерчанта (/portal → Profile) или тем же
PATCH-вызовом; ротация секрета — POST /v1/merchants/{id}/rotate-webhook-secret
(новый секрет показывается один раз) или кнопка Rotate secret во
вкладке Profile портала.
Обработка на вашей стороне (обязательно):
PA-Webhook-Signature: v1=<hex(HMAC-SHA256(raw_body, webhook_secret))>
над сырым телом запроса;event_id;payment.authorized / payment.captured → зачисление игроку;Типы событий: payment.authorized, payment.captured, payment.refunded, payment.failed.
Доставка — at-least-once: ретраи до DELIVERED, после
исчерпания — DEAD; журнал доставок — вкладка Integration портала); статус
платежа — источник истины через polling.
Через PA API tenant-ключом оператора (merchant-scoped mer_sk_… казино
онбординг не выполняет — только собственные платежи):
POST /v1/merchants {"name": "My Casino", "country": "CY", "currencies": ["EUR"]}
POST /v1/merchants/{id}/providers {"provider_code": "payadmit", "mid": "mid_1", "credentials_ref": "mca_1", "currencies": ["EUR"]}
POST /v1/merchants/{id}/route-targets {"provider_account_id": "mid_...", "priority": 1, "filters": {"currency": ["EUR"], "method": ["automatic"]}}
POST /v1/merchants/{id}/webhook {"url": "https://casino.example/pa/webhook"}
Ответ POST /v1/merchants содержит publishable_key (pk_<merchant-uuid>_<digest>) — публичный ключ для PASecureFields.mount({publishableKey}). Он детерминированный:
GET /v1/merchants/{id} возвращает тот же ключ (потерять его нельзя, ротация не нужна).
Сменить привязку/отозвать ключ можно перенастройкой pepper на стороне PA (все ключи
мерчанта меняются одномоментно).
Тестовое казино целиком (frontend + backend + webhook receiver) — pa-sdk/demo/server.js:
# 1. Поднять PA (mock engine, mock vault) на disposable PG
# (из каталога развёртывания PA Core)
PA_DATABASE_URL=postgres://pa:pa@127.0.0.1:5432/pa?sslmode=disable \
PA_ENGINE=mock PA_BIND=127.0.0.1:8080 cargo run --bin pa-api
# 2. Создать tenant-ключ (seed) и запустить демо-казино
PA_SEED_TENANT_API_KEY=pa_sk_demo_0000000000000000000000000000000000000000000000000000000000 \
PA_DATABASE_URL=postgres://pa:pa@127.0.0.1:5432/pa?sslmode=disable cargo run --bin pa-seed
# → stdout: tenant_id=... merchant_id=...
cd pa-sdk/demo
PA_BASE_URL=http://127.0.0.1:8080 \
PA_TENANT_KEY=pa_sk_demo_0000000000000000000000000000000000000000000000000000000000 \
node server.js
# → demo casino running at http://127.0.0.1:8090/demo/
Откройте http://127.0.0.1:8090/demo/:
4111 1111 1111 1111, 12/30, CVC 123 → Tokenize card —
в demo-режиме capture-поверхности токен mintится внутри iframe (без сети);AUTHORIZED в логе шага 3;payment.authorized в журнале шага 4.PCI-наблюдение: в логах демо-сервера и PA нет ни PAN, ни CVC, ни vault token — только
pin_..., pay_... и метаданные ****1111.
Два режима captureUrl:
| Режим | captureUrl |
Что происходит |
|---|---|---|
| demo (CI/E2E/dev) | 'demo' |
Токен vtok_<brand>_<last4>-<uuid> mintится в iframe без сети, PAN/CVV умирают в iframe. Publishable key не нужен. |
| прод (реальный Locker) | https://<pa-origin>/v1/sdk/capture |
iframe шлёт PAN/CVV на capture-endpoint PA → fork (POST /payment_methods, JWE→Locker) → pm_... + маскированные метаданные. Требуется publishableKey. |
Прод-требования со стороны PA: PA_CARD_VAULT_MODE=fork,
HTTPS-only на публичных адресах (fail-fast при PA_ENV=production), жёсткий rate limit на ключ
(PA_SDK_CAPTURE_RPM, по умолчанию 30/мин) + per-IP окно; при необходимости — origin allowlist
(PA_SDK_CAPTURE_ALLOWED_ORIGINS).
Справочник полей создания платежа: типы, обязательность, ограничения и коды ошибок,
которые платформа отвечает на нарушение каждого правила. Дополнение к гайду
SDK + Webhooks (токенизация → инструмент → платёж), к
Handover Package (ключи и 6 шагов интеграции) и к
чек-листу онбординга. Машиночитаемый контракт —
openapi.json (GET /docs/openapi.json), RU-описание ресурса —
payments-api.md.
Пример запроса целиком:
{
"merchant_id": "mer_...",
"amount": 1050,
"currency": "EUR",
"payment_method": { "type": "card", "instrument_id": "pin_..." },
"capture_method": "automatic",
"customer": {
"reference_id": "usr_123",
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com",
"phone": "35712345678"
},
"billing_address": {
"address_line1": "211 Victory st",
"city": "Limassol",
"postal_code": "3011",
"country_code": "CY"
},
"session_context": {
"browser": { "user_agent": "Mozilla/5.0...", "time_zone": -120 }
},
"metadata": { "order_reference": "ord-2026-000123" }
}
| Поле | Тип | Обязательность | Назначение и ограничения |
|---|---|---|---|
merchant_id |
string | да | Ваш мерчант (mer_…) в тенанте. Чужой/неизвестный — 422 unknown_merchant |
amount |
integer | да | Положительное число в minor units (центы). ≤ 0 → 422 invalid_request |
currency |
string | да | ISO 4217 alpha-3 (EUR, USD, …). Валюта участвует в eligibility: ни один Route Target не поддерживает валюту → 422 no_eligible_route |
payment_method |
object | нет¹ | Выбор способа платежа — см. §2. Без payment_method платёж создаётся (201), но исполнение не состоится: диспатч коннектора требует instrument_id и попытка завершится FAILED_TECHNICAL |
capture_method |
string | нет (automatic по умолчанию) |
automatic | manual — см. §3 |
customer |
object | нет | Данные игрока для PSP — см. §4. Должен быть JSON-объектом (или null/отсутствовать); строка/число/массив → 422 invalid_request |
billing_address |
object | нет² | Биллинг-адрес для PSP и роутинга — см. §5 |
risk_context |
object | нет | Свободная форма (KYC-флаги, статистика депозитов). Сохраняется; на MVP-роутинг не влияет — см. §6 |
session_context |
object | нет | Сессия игрока; блок browser проецируется в PSP — см. §6 |
metadata |
object | нет | Свободная форма (additionalProperties: true); соглашение metadata.order_reference — см. §7 |
¹ Платёж по карте диспатчится только через инструмент: payment_method.instrument_id
обязателен для фактического исполнения (surface pin_… — §2).
² Необязательно по схеме, но строго рекомендован: см. §5 (роутинг по стране и
обязательные поля PSP DEPOSIT-профиля).
Заголовок Idempotency-Key обязателен для POST; канонический хеш считается по
разобранной структуре запроса, поэтому тела с разным порядком ключей, но одинаковой
семантикой дают один хеш. Повтор тем же ключом+телом реплеит сохранённый результат;
тот же ключ с другим телом — 422 idempotency_key_reuse.
| Поле | Значения | Ограничения | Ошибка при нарушении |
|---|---|---|---|
type |
"card" — единственное поддерживаемое значение |
Любое другое значение отклоняется контрактом платформы | 422 unsupported_payment_method |
instrument_id |
pin_… — идентификатор из POST /v1/payment-instruments |
SINGLE-USE: регистрируется от single-use vault token (Secure Fields capture), потребляется атомарно с созданием платежа | 422 unknown_instrument (нет в тенанте), 422 instrument_merchant_mismatch (инструмент другого мерчанта тенанта), 422 instrument_not_active (уже использован USED или деактивирован) |
Порядок интеграции: карта игрока → Secure Fields SDK → single-use vault token →
POST /v1/payment-instruments (pin_…) → payment_method.instrument_id здесь.
Повторное использование того же pin_… для второго платежа невозможно: статус
инструмента становится USED, повтор отвечает 422 instrument_not_active — для
повторного депозита зарегистрируйте новый инструмент от нового vault token
(сохранённые карты — тот же путь: одна регистрация = один платёж).
PAN/CVV в этот запрос не принимаются никогда: поля карты живут только внутри capture-поверхности (см. гайд SDK + Webhooks §1/§4).
| Значение | Семантика | Комментарий |
|---|---|---|
automatic (default) |
Sale-модель: PSP диспатчит PURC, авторизация и захват — одним вызовом | PA-статус AUTHORIZED = provider_status COMPLETED: деньги уже захвачены PSP, депозит можно зачислять игроку по webhook payment.authorized |
manual |
Authorize-only: платёж останавливается до отдельного захвата | Capture-endpoint в публичном API не экспонирован (/v1/payments/{id}/capture не входит в текущую поверхность). Используйте automatic |
capture_method валидируется по capabilities Route Target: target без поддержки
запрошенного метода отфильтровывается на eligibility. Текущие merchant-роуты пилота
настроены на automatic — запрос manual без способного target отвечает
422 no_eligible_route (без попыток исполнения).
next_action в ответе на POST /v1/payments и в GET /v1/payments/{id} сообщает,
что клиент должен сделать дальше. В текущих live-флоу встречаются значения:
| Значение | Что делать |
|---|---|
none |
Ничего: платёж идёт сам, финал придёт webhook'ом (payment.*) или через GET /v1/payments/{id} |
wait |
Подождать retry_after_secs и опросить GET /v1/payments/{id} (идёт синхронизация статуса) |
redirect |
Перевести игрока по redirect_url (3DS challenge / hosted page PSP); возврат игрока — не результат, финал — webhook/polling — см. шаг К5 чек-листа онбординга |
collect_data |
Контрактный member для PSP-флоу с дополнительным вводом — см. ниже |
collect_data означает, что PSP просит собрать у игрока дополнительные данные,
прежде чем платёж сможет продолжиться:
required_fields — список имён полей, которые PSP просит собрать
(например email, phone); состав определяется PSP-процессором платежа;POST /v1/payments) либо повторяет
confirm-вызов, передав запрошенные данные;none/redirect/wait; collect_data зарезервирован контрактом заранее —
клиент интеграции должен уметь обрабатывать это значение так же, как
redirect: не считать его финалом и не кредитовать игрока до финального
статуса.Пример: {"type": "collect_data", "required_fields": ["email", "phone"]} —
соберите у игрока email и телефон и повторите создание платежа, передав их в
блоке customer (§4).
Свободная форма сохраняется целиком (write-only — см. §8), но эти поля читает
платформа и проецирует в PSP-вызов (billing форка: address.first_name,
address.last_name, email, phone):
| Поле | Тип | Обязательность | Комментарий |
|---|---|---|---|
reference_id |
string | number | нет (рекомендуется) | ID игрока у казино — ownership-контекст инструментов. НЕ PAN: строка или число канонизируется и проходит Luhn-пробу — Luhn-валидная 13–19-значная последовательность (в т.ч. форматированная 4111-1111-1111-1111) отклоняется как кард-данные. Ограничения: ≤255 символов, charset [A-Za-z0-9_.@-]; null = поле отсутствует; другой JSON-тип → 422 invalid_request |
first_name / last_name |
string | да для Apcopay S2S | Имя/фамилия игрока (проецируются в billing.address) |
email |
string | да для Apcopay S2S | Проекция в billing.email и верхнеуровневый email PSP |
phone |
string | object | да для Apcopay S2S | Плоская строка — только цифры, без + (ведущий + допустим и обрезается: "+35712345678" → 35712345678; лучше сразу слать цифры); структурированная форма { "country_code": "357", "number": "12345678" } проецируется verbatim. Без phone коннектор Apcopay S2S отвергает платёж ошибкой missing-required-param (семейство IR_04 форка) — профиль PSP DEPOSIT требует customer.phone наряду с именем и email |
Телефон/имя/email нужны на каждый S2S-платёж (профиль PSP), а не только на
регистрацию инструмента: POST /v1/payment-instruments их не принимает — платёжный
запрос единственное место, где PA видит контекст игрока.
Проецируется в billing.address PSP; коды маппинга PA → PSP:
address_line1→line1, address_line2→line2, city, state, postal_code→zip,
country_code или country→country.
| Поле | Тип | Обязательность | Комментарий |
|---|---|---|---|
address_line1 |
string | да для Apcopay S2S | Строка адреса; PSP DEPOSIT-профиль требует addressLine1 |
address_line2 |
string | нет | Доп. строка |
city |
string | да для Apcopay S2S | |
state |
string | нет | Штат/регион |
postal_code |
string | да для Apcopay S2S | |
country_code (или country) |
string | см. ниже | ISO-2 (CY, MT, …). Читается роутингом |
Две независимые причины заполнять:
422 no_eligible_route. Валютный eligibility без
страны работает, но список маршрутов может быть уже ожидаемого.addressLine1, city, countryCode, postalCode) — без блока
billing_address платёж уходит без billing-данных и коннектор может отвергнуть
его на своей валидации.Оба блока свободной формы: сохраняются с платежём, на MVP-роутинг не влияют (эластичность политик — следующий шаг). Исключение — блок браузера:
session_context.browser проецируется в browser_info PSP-вызова (3DS-данные):
Поле блока browser |
Тип | Комментарий |
|---|---|---|
user_agent |
string | Apcopay DirectConnect PURC требует UserAgent |
time_zone |
number | JS getTimezoneOffset в минутах (минус — восточнее UTC); Apcopay требует TimeZone |
accept_header, language |
string | опционально |
java_enabled, java_script_enabled |
boolean | опционально |
screen_width, screen_height, color_depth |
integer | опционально |
ip_address |
string | IP игрока; должен парситься как IP |
Без session_context.browser платёж диспатчится без browser-данных, и отсутствие
UserAgent/TimeZone — типичная причина отклонения Apcopay PURC: собирайте
блок на клиенте и передавайте всегда.
metadata — свободная форма, сохраняется с платежём. Соглашение платформы:
внешний ID заказа кладите в metadata.order_reference — тогда он появляется
в мерчант-портале (колонка Order Reference списка транзакций, свободный поиск q
и CSV-экспорт report.csv). Передавать его отдельным полем не нужно — отдельного
поля в контракте нет.
Ответ (201 creation и GET /v1/payments/{id}) — проекция логического платежа:
{
"payment_id": "pay_...",
"status": "AUTHORIZED",
"amount": 1050,
"currency": "EUR",
"capture_method": "automatic",
"attempts_summary": [... ],
"created_at": "2026-09-20T12:00:00Z"
}
Поля customer, billing_address, metadata, risk_context, session_context
— write-only: они не возвращаются в ответе Payments API. Это дисциплина
данных платформы: маскированные/чувствительные контексты не гуляют по публичному
API. Доступ к ним — мерчант-портал: Transaction Details
(GET /v1/merchants/{id}/payments/{payment_id} возвращает metadata и customer
verbatim) и CSV-экспорт (report.csv).
| Код | HTTP | Когда |
|---|---|---|
unsupported_payment_method |
422 | payment_method.type ≠ card |
unknown_instrument |
422 | pin_… нет в тенанте |
instrument_merchant_mismatch |
422 | инструмент другого мерчанта тенанта |
instrument_not_active |
422 | инструмент уже использован (USED) или деактивирован — SINGLE-USE |
no_eligible_route |
422 | ни один Route Target не поддерживает валюту/capture_method/страну (пустая country-filter выборка) |
unknown_merchant |
422 | merchant_id не в тенанте ключа |
invalid_request |
400/422 | amount ≤ 0, кривая валюта, customer не объект, нарушение формы customer.reference_id (charset/длина/PAN-проба) |
idempotency_key_required |
400 | нет заголовка Idempotency-Key |
idempotency_key_reuse |
422 | тот же ключ с другим телом |
Детерминированные коды сохраняются в idempotency-записи и реплеятся тем же ключом (повтор возвращает сохранённый результат, включая ошибку). Полный список и семафоры — payments-api.md §3.
Справочник полей создания платежа: типы, обязательность, ограничения и коды ошибок,
которые платформа отвечает на нарушение каждого правила. Дополнение к гайду
SDK + Webhooks (токенизация → инструмент → платёж), к
Handover Package (ключи и 6 шагов интеграции) и к
чек-листу онбординга. Машиночитаемый контракт —
openapi.json (GET /docs/openapi.json), RU-описание ресурса —
payments-api.md.
Пример запроса целиком:
{
"merchant_id": "mer_...",
"amount": 1050,
"currency": "EUR",
"payment_method": { "type": "card", "instrument_id": "pin_..." },
"capture_method": "automatic",
"customer": {
"reference_id": "usr_123",
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com",
"phone": "35712345678"
},
"billing_address": {
"address_line1": "211 Victory st",
"city": "Limassol",
"postal_code": "3011",
"country_code": "CY"
},
"session_context": {
"browser": { "user_agent": "Mozilla/5.0...", "time_zone": -120 }
},
"metadata": { "order_reference": "ord-2026-000123" }
}
| Поле | Тип | Обязательность | Назначение и ограничения |
|---|---|---|---|
merchant_id |
string | да | Ваш мерчант (mer_…) в тенанте. Чужой/неизвестный — 422 unknown_merchant |
amount |
integer | да | Положительное число в minor units (центы). ≤ 0 → 422 invalid_request |
currency |
string | да | ISO 4217 alpha-3 (EUR, USD, …). Валюта участвует в eligibility: ни один Route Target не поддерживает валюту → 422 no_eligible_route |
payment_method |
object | нет¹ | Выбор способа платежа — см. §2. Без payment_method платёж создаётся (201), но исполнение не состоится: диспатч коннектора требует instrument_id и попытка завершится FAILED_TECHNICAL |
capture_method |
string | нет (automatic по умолчанию) |
automatic | manual — см. §3 |
customer |
object | нет | Данные игрока для PSP — см. §4. Должен быть JSON-объектом (или null/отсутствовать); строка/число/массив → 422 invalid_request |
billing_address |
object | нет² | Биллинг-адрес для PSP и роутинга — см. §5 |
risk_context |
object | нет | Свободная форма (KYC-флаги, статистика депозитов). Сохраняется; на MVP-роутинг не влияет — см. §6 |
session_context |
object | нет | Сессия игрока; блок browser проецируется в PSP — см. §6 |
metadata |
object | нет | Свободная форма (additionalProperties: true); соглашение metadata.order_reference — см. §7 |
¹ Платёж по карте диспатчится только через инструмент: payment_method.instrument_id
обязателен для фактического исполнения (surface pin_… — §2).
² Необязательно по схеме, но строго рекомендован: см. §5 (роутинг по стране и
обязательные поля PSP DEPOSIT-профиля).
Заголовок Idempotency-Key обязателен для POST; канонический хеш считается по
разобранной структуре запроса, поэтому тела с разным порядком ключей, но одинаковой
семантикой дают один хеш. Повтор тем же ключом+телом реплеит сохранённый результат;
тот же ключ с другим телом — 422 idempotency_key_reuse.
| Поле | Значения | Ограничения | Ошибка при нарушении |
|---|---|---|---|
type |
"card" — единственное поддерживаемое значение |
Любое другое значение отклоняется контрактом платформы | 422 unsupported_payment_method |
instrument_id |
pin_… — идентификатор из POST /v1/payment-instruments |
SINGLE-USE: регистрируется от single-use vault token (Secure Fields capture), потребляется атомарно с созданием платежа | 422 unknown_instrument (нет в тенанте), 422 instrument_merchant_mismatch (инструмент другого мерчанта тенанта), 422 instrument_not_active (уже использован USED или деактивирован) |
Порядок интеграции: карта игрока → Secure Fields SDK → single-use vault token →
POST /v1/payment-instruments (pin_…) → payment_method.instrument_id здесь.
Повторное использование того же pin_… для второго платежа невозможно: статус
инструмента становится USED, повтор отвечает 422 instrument_not_active — для
повторного депозита зарегистрируйте новый инструмент от нового vault token
(сохранённые карты — тот же путь: одна регистрация = один платёж).
PAN/CVV в этот запрос не принимаются никогда: поля карты живут только внутри capture-поверхности (см. гайд SDK + Webhooks §1/§4).
| Значение | Семантика | Комментарий |
|---|---|---|
automatic (default) |
Sale-модель: PSP диспатчит PURC, авторизация и захват — одним вызовом | PA-статус AUTHORIZED = provider_status COMPLETED: деньги уже захвачены PSP, депозит можно зачислять игроку по webhook payment.authorized |
manual |
Authorize-only: платёж останавливается до отдельного захвата | Capture-endpoint в публичном API не экспонирован (/v1/payments/{id}/capture не входит в текущую поверхность). Используйте automatic |
capture_method валидируется по capabilities Route Target: target без поддержки
запрошенного метода отфильтровывается на eligibility. Текущие merchant-роуты пилота
настроены на automatic — запрос manual без способного target отвечает
422 no_eligible_route (без попыток исполнения).
next_action в ответе на POST /v1/payments и в GET /v1/payments/{id} сообщает,
что клиент должен сделать дальше. В текущих live-флоу встречаются значения:
| Значение | Что делать |
|---|---|
none |
Ничего: платёж идёт сам, финал придёт webhook'ом (payment.*) или через GET /v1/payments/{id} |
wait |
Подождать retry_after_secs и опросить GET /v1/payments/{id} (идёт синхронизация статуса) |
redirect |
Перевести игрока по redirect_url (3DS challenge / hosted page PSP); возврат игрока — не результат, финал — webhook/polling — см. шаг К5 чек-листа онбординга |
collect_data |
Контрактный member для PSP-флоу с дополнительным вводом — см. ниже |
collect_data означает, что PSP просит собрать у игрока дополнительные данные,
прежде чем платёж сможет продолжиться:
required_fields — список имён полей, которые PSP просит собрать
(например email, phone); состав определяется PSP-процессором платежа;POST /v1/payments) либо повторяет
confirm-вызов, передав запрошенные данные;none/redirect/wait; collect_data зарезервирован контрактом заранее —
клиент интеграции должен уметь обрабатывать это значение так же, как
redirect: не считать его финалом и не кредитовать игрока до финального
статуса.Пример: {"type": "collect_data", "required_fields": ["email", "phone"]} —
соберите у игрока email и телефон и повторите создание платежа, передав их в
блоке customer (§4).
Свободная форма сохраняется целиком (write-only — см. §8), но эти поля читает
платформа и проецирует в PSP-вызов (billing форка: address.first_name,
address.last_name, email, phone):
| Поле | Тип | Обязательность | Комментарий |
|---|---|---|---|
reference_id |
string | number | нет (рекомендуется) | ID игрока у казино — ownership-контекст инструментов. НЕ PAN: строка или число канонизируется и проходит Luhn-пробу — Luhn-валидная 13–19-значная последовательность (в т.ч. форматированная 4111-1111-1111-1111) отклоняется как кард-данные. Ограничения: ≤255 символов, charset [A-Za-z0-9_.@-]; null = поле отсутствует; другой JSON-тип → 422 invalid_request |
first_name / last_name |
string | да для Apcopay S2S | Имя/фамилия игрока (проецируются в billing.address) |
email |
string | да для Apcopay S2S | Проекция в billing.email и верхнеуровневый email PSP |
phone |
string | object | да для Apcopay S2S | Плоская строка — только цифры, без + (ведущий + допустим и обрезается: "+35712345678" → 35712345678; лучше сразу слать цифры); структурированная форма { "country_code": "357", "number": "12345678" } проецируется verbatim. Без phone коннектор Apcopay S2S отвергает платёж ошибкой missing-required-param (семейство IR_04 форка) — профиль PSP DEPOSIT требует customer.phone наряду с именем и email |
Телефон/имя/email нужны на каждый S2S-платёж (профиль PSP), а не только на
регистрацию инструмента: POST /v1/payment-instruments их не принимает — платёжный
запрос единственное место, где PA видит контекст игрока.
Проецируется в billing.address PSP; коды маппинга PA → PSP:
address_line1→line1, address_line2→line2, city, state, postal_code→zip,
country_code или country→country.
| Поле | Тип | Обязательность | Комментарий |
|---|---|---|---|
address_line1 |
string | да для Apcopay S2S | Строка адреса; PSP DEPOSIT-профиль требует addressLine1 |
address_line2 |
string | нет | Доп. строка |
city |
string | да для Apcopay S2S | |
state |
string | нет | Штат/регион |
postal_code |
string | да для Apcopay S2S | |
country_code (или country) |
string | см. ниже | ISO-2 (CY, MT, …). Читается роутингом |
Две независимые причины заполнять:
422 no_eligible_route. Валютный eligibility без
страны работает, но список маршрутов может быть уже ожидаемого.addressLine1, city, countryCode, postalCode) — без блока
billing_address платёж уходит без billing-данных и коннектор может отвергнуть
его на своей валидации.Оба блока свободной формы: сохраняются с платежём, на MVP-роутинг не влияют (эластичность политик — следующий шаг). Исключение — блок браузера:
session_context.browser проецируется в browser_info PSP-вызова (3DS-данные):
Поле блока browser |
Тип | Комментарий |
|---|---|---|
user_agent |
string | Apcopay DirectConnect PURC требует UserAgent |
time_zone |
number | JS getTimezoneOffset в минутах (минус — восточнее UTC); Apcopay требует TimeZone |
accept_header, language |
string | опционально |
java_enabled, java_script_enabled |
boolean | опционально |
screen_width, screen_height, color_depth |
integer | опционально |
ip_address |
string | IP игрока; должен парситься как IP |
Без session_context.browser платёж диспатчится без browser-данных, и отсутствие
UserAgent/TimeZone — типичная причина отклонения Apcopay PURC: собирайте
блок на клиенте и передавайте всегда.
metadata — свободная форма, сохраняется с платежём. Соглашение платформы:
внешний ID заказа кладите в metadata.order_reference — тогда он появляется
в мерчант-портале (колонка Order Reference списка транзакций, свободный поиск q
и CSV-экспорт report.csv). Передавать его отдельным полем не нужно — отдельного
поля в контракте нет.
Ответ (201 creation и GET /v1/payments/{id}) — проекция логического платежа:
{
"payment_id": "pay_...",
"status": "AUTHORIZED",
"amount": 1050,
"currency": "EUR",
"capture_method": "automatic",
"attempts_summary": [... ],
"created_at": "2026-09-20T12:00:00Z"
}
Поля customer, billing_address, metadata, risk_context, session_context
— write-only: они не возвращаются в ответе Payments API. Это дисциплина
данных платформы: маскированные/чувствительные контексты не гуляют по публичному
API. Доступ к ним — мерчант-портал: Transaction Details
(GET /v1/merchants/{id}/payments/{payment_id} возвращает metadata и customer
verbatim) и CSV-экспорт (report.csv).
| Код | HTTP | Когда |
|---|---|---|
unsupported_payment_method |
422 | payment_method.type ≠ card |
unknown_instrument |
422 | pin_… нет в тенанте |
instrument_merchant_mismatch |
422 | инструмент другого мерчанта тенанта |
instrument_not_active |
422 | инструмент уже использован (USED) или деактивирован — SINGLE-USE |
no_eligible_route |
422 | ни один Route Target не поддерживает валюту/capture_method/страну (пустая country-filter выборка) |
unknown_merchant |
422 | merchant_id не в тенанте ключа |
invalid_request |
400/422 | amount ≤ 0, кривая валюта, customer не объект, нарушение формы customer.reference_id (charset/длина/PAN-проба) |
idempotency_key_required |
400 | нет заголовка Idempotency-Key |
idempotency_key_reuse |
422 | тот же ключ с другим телом |
Детерминированные коды сохраняются в idempotency-записи и реплеятся тем же ключом (повтор возвращает сохранённый результат, включая ошибку). Полный список и семафоры — payments-api.md §3.
Payment lifecycle (create, read). Status machine: PENDING → AUTHORIZED → CAPTURED → PARTIALLY_REFUNDED → REFUNDED; FAILED; UNKNOWN_PENDING_SYNC.
Idempotent payment creation. The request runs the routing cascade (eligible Route Targets by currency/capture method) through the execution engine; the response is the logical payment of the PA model (attempt journal collapsed into attempts_summary, no PSP internals).
Deterministic client errors (unknown_merchant, no_eligible_route, validation failures) are stored with the idempotency record and are replayed identically on retry with the same key.
Requires the Idempotency-Key header.
| Idempotency-Key required | string [ 1 .. 255 ] characters Client-generated unique key of the operation. A retry with the same key + same body replays the stored result of the original request — the ORIGINAL status code (e.g. |
| merchant_id required | string Merchant of this tenant ( |
| amount required | integer >= 1 Positive integer in minor units (cents); |
| currency required | string ISO 4217 alpha-3. Part of the routing eligibility: when no Route Target of the merchant supports the currency, the payment answers |
object Payment method selection. Card payments reference a PA instrument ( | |
| capture_method | string Default: "automatic" enum: "automatic" "manual"
|
object Customer context (identity/contact); risk fields belong to | |
| risk_context | object Casino-supplied risk context (KYC flags, deposit statistics). Free-form, stored with the payment, write-only (never echoed). The MVP routing does not consume it; the connector projection is data-minimized. |
object Device/session context of the payment. Free-form, stored, write-only (never echoed); it does not affect the MVP routing — EXCEPT the | |
object Billing address; free-form and write-only (never echoed by the payment response). Two consumers: (1) the routing country filter — | |
object Free-form client metadata (e.g. |
| Idempotent-Replayed | string enum: "true" "false"
|
| payment_id required | string |
| status required | string enum: "PENDING" "AUTHORIZED" "CAPTURED" "PARTIALLY_REFUNDED" "REFUNDED" "FAILED" "UNKNOWN_PENDING_SYNC" PA payment status. |
| amount required | integer |
| currency required | string |
| capture_method required | string enum: "automatic" "manual" |
object or object or object or object or object (NextAction) Typed union ( | |
required | Array of objects Collapsed attempt journal: per-attempt sequence number, the public Route Target projection (provider + MID label only, no PSP secrets) and the attempt status. The decline class is NOT part of the summary — it is carried by the full attempt domain model internally, never by this public projection. |
| created_at required | string <date-time> |
{- "merchant_id": "string",
- "amount": 1050,
- "currency": "EUR",
- "payment_method": {
- "type": "card",
- "instrument_id": "pin_01900000-0000-7000-8000-000000000001"
}, - "capture_method": "automatic",
- "customer": {
- "reference_id": "usr_123",
- "first_name": "string",
- "last_name": "string",
- "email": "string",
- "phone": "35712345678"
}, - "risk_context": { },
- "session_context": {
- "browser": { }
}, - "billing_address": {
- "address_line1": "string",
- "address_line2": "string",
- "city": "string",
- "state": "string",
- "postal_code": "string",
- "country_code": "string",
- "country": "string"
}, - "metadata": { }
}{- "payment_id": "pay_01900000-0000-7000-8000-00000000000a",
- "status": "PENDING",
- "amount": 1050,
- "currency": "EUR",
- "capture_method": "automatic",
- "next_action": {
- "type": "none"
}, - "attempts_summary": [
- {
- "seq": 0,
- "route_target": {
- "provider": "payadmit",
- "mid": "mid_1"
}, - "status": "PENDING"
}
], - "created_at": "2019-08-24T14:15:22Z"
}Returns the last confirmed state of the payment with the collapsed attempt journal. Fresh reads never call the PSP; while the state is PENDING or UNKNOWN_PENDING_SYNC the response carries next_action and a Retry-After header (synchronization is asynchronous — the source of truth is this GET).
| id required | string Payment id ( |
| Retry-After | integer Seconds the client should wait before retrying. |
| payment_id required | string |
| status required | string enum: "PENDING" "AUTHORIZED" "CAPTURED" "PARTIALLY_REFUNDED" "REFUNDED" "FAILED" "UNKNOWN_PENDING_SYNC" PA payment status. |
| amount required | integer |
| currency required | string |
| capture_method required | string enum: "automatic" "manual" |
object or object or object or object or object (NextAction) Typed union ( | |
required | Array of objects Collapsed attempt journal: per-attempt sequence number, the public Route Target projection (provider + MID label only, no PSP secrets) and the attempt status. The decline class is NOT part of the summary — it is carried by the full attempt domain model internally, never by this public projection. |
| created_at required | string <date-time> |
{- "payment_id": "pay_01900000-0000-7000-8000-00000000000a",
- "status": "PENDING",
- "amount": 1050,
- "currency": "EUR",
- "capture_method": "automatic",
- "next_action": {
- "type": "none"
}, - "attempts_summary": [
- {
- "seq": 0,
- "route_target": {
- "provider": "payadmit",
- "mid": "mid_1"
}, - "status": "PENDING"
}
], - "created_at": "2019-08-24T14:15:22Z"
}Idempotent full or partial refund of a payment in AUTHORIZED / CAPTURED / PARTIALLY_REFUNDED / REFUNDED state. The refund amount never exceeds the remaining refundable amount; the payment status is recomputed (PARTIALLY_REFUNDED → REFUNDED). Requires the Idempotency-Key header.
| Idempotency-Key required | string [ 1 .. 255 ] characters Client-generated unique key of the operation. A retry with the same key + same body replays the stored result of the original request — the ORIGINAL status code (e.g. |
| payment_id required | string |
| amount required | integer >= 1 Minor units; must not exceed the remaining refundable amount. |
| reason | string |
| Idempotent-Replayed | string enum: "true" "false"
|
| refund_id required | string |
| payment_id required | string |
| amount required | integer |
| currency required | string |
| status required | string enum: "PENDING" "SUCCEEDED" "FAILED" Refund status. |
| payment_status required | string enum: "AUTHORIZED" "CAPTURED" "PARTIALLY_REFUNDED" "REFUNDED" "FAILED" Recomputed payment status. |
{- "payment_id": "pay_01900000-0000-7000-8000-00000000000a",
- "amount": 1,
- "reason": "string"
}{- "refund_id": "string",
- "payment_id": "string",
- "amount": 0,
- "currency": "string",
- "status": "PENDING",
- "payment_status": "AUTHORIZED"
}Card instrument registration from single-use vault tokens (Secure Fields capture surface). No PAN/CVV ever reaches the backend API.
PCI boundary: the browser-side capture surface (PA Secure Fields SDK → PA Locker) tokenizes PAN/CVV into a single-use vault_token. This endpoint registers the instrument in PA — the request struct carries NO card fields and cannot accept PAN/CVV; PA resolves the masked metadata (masked PAN, brand, expiry) from the vault itself. The vault token is a credential: it is consumed for the metadata lookup and stored as a reference only, never serialized back.
Requires the Idempotency-Key header.
| Idempotency-Key required | string [ 1 .. 255 ] characters Client-generated unique key of the operation. A retry with the same key + same body replays the stored result of the original request — the ORIGINAL status code (e.g. |
| merchant_id required | string |
| vault_token required | string Opaque single-use token from the Secure Fields capture surface (credential — never logged, never serialized back). |
| customer_reference | string Casino-side player reference (ownership context; optional). |
| Idempotent-Replayed | string enum: "true" "false"
|
| instrument_id required | string |
| merchant_id required | string |
| status required | string enum: "ACTIVE" "USED" "EXPIRED" "DEACTIVATED" "PURGED" |
required | object (CardMetaResponse) |
| customer_reference | string |
| created_at required | string <date-time> |
| used_at | string <date-time> First payment usage of the single-use instrument. |
{- "merchant_id": "string",
- "vault_token": "string",
- "customer_reference": "string"
}{- "instrument_id": "pin_01900000-0000-7000-8000-000000000001",
- "merchant_id": "string",
- "status": "ACTIVE",
- "card_meta": {
- "masked_pan": "****4242",
- "brand": "VISA",
- "exp_month": "string",
- "exp_year": "string"
}, - "customer_reference": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "used_at": "2019-08-24T14:15:22Z"
}Returns masked card metadata only (masked PAN ****<last4>, brand, optional expiry) and the instrument lifecycle status. No vault token is ever returned.
| id required | string Payment instrument id ( |
| instrument_id required | string |
| merchant_id required | string |
| status required | string enum: "ACTIVE" "USED" "EXPIRED" "DEACTIVATED" "PURGED" |
required | object (CardMetaResponse) |
| customer_reference | string |
| created_at required | string <date-time> |
| used_at | string <date-time> First payment usage of the single-use instrument. |
{- "instrument_id": "pin_01900000-0000-7000-8000-000000000001",
- "merchant_id": "string",
- "status": "ACTIVE",
- "card_meta": {
- "masked_pan": "****4242",
- "brand": "VISA",
- "exp_month": "string",
- "exp_year": "string"
}, - "customer_reference": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "used_at": "2019-08-24T14:15:22Z"
}Merchant self-service credential lifecycle: webhook-secret rotation and API-key rotation (the new raw key is shown exactly once; the replaced key keeps a 24h grace window). Merchant creation and provider/route configuration are operator surfaces and are not part of this reference.
Replaces the HMAC signing secret of the registered webhook endpoint. The NEW secret is generated server-side and returned EXACTLY ONCE in this response — deliveries are signed with it immediately, while the replaced secret stays verifiable receiver-side until old_secret_expires_at (PA_WEBHOOK_SECRET_GRACE_DAYS, default 7 days): verify BOTH secrets during the grace window (dual-verify). Audited as rotate_webhook_secret (details carry the URL and the rotation time, never a secret).
| id required | string Merchant id ( |
| merchant_id required | string |
| webhook_url required | string |
| webhook_secret required | string The NEW active HMAC signing secret ( |
| old_secret_expires_at required | string RFC 3339 end of the receiver-side grace window of the replaced secret: verify BOTH secrets until it (dual-verify), only the new one after. |
| rotated_at required | string RFC 3339 rotation time. |
{- "merchant_id": "string",
- "webhook_url": "string",
- "webhook_secret": "string",
- "old_secret_expires_at": "string",
- "rotated_at": "string"
}Generates a NEW merchant-scoped key, returns it EXACTLY ONCE and keeps the replaced key verifiable until old_key_expires_at. The merchant's own scoped key may rotate ITSELF; the admin key may rotate any tenant merchant; a merchant-scoped key on a foreign merchant is a plain 404. Audited merchant.api_key_rotated (hashes + grace expiry only).
| id required | string Merchant id ( |
| merchant_id required | string Merchant id ( |
| api_key required | string The raw merchant-scoped API key ( |
| created_at required | string RFC 3339 issue/rotate time of the key. |
{- "merchant_id": "string",
- "api_key": "string",
- "created_at": "string"
}Self-service surface of the casino merchant portal: dashboard statistics, payments list, payment details, CSV report, webhook delivery health, the self-profile read and the webhook URL re-target. MVP auth: the same admin Bearer as the operator surfaces (single-principal model) — a merchant-scoped portal key is the production follow-up.
Dashboard aggregates of one merchant over a trailing window (7d / 30d, default 7d): Turnover EUR (with the previous-period delta), Approval Ratio, Transaction Count, Pay Ins, Credits/Refunds, Payment Option Distribution, the full payment-state histogram and the daily rollup (the Reconciliation view).
Semantics: the approved family is AUTHORIZED, CAPTURED, PARTIALLY_REFUNDED, REFUNDED (refunds are approvals returned later); turnover_eur sums the AUTHORIZED | CAPTURED volume in MINOR units (cents — the same unit as every amount in this API) with currency EUR only — money IN, while refunded volume is reported as Credits/Refunds (refunds_eur, SUCCEEDED refunds from the refunds table); other currencies are reported per-currency in turnover_by_currency and are never mixed; previous_turnover_eur is the same aggregate over the PREVIOUS period of the same length ([now-2N, now-N)) — the delta basis of the Turnover card; approval_ratio is approved / (approved + FAILED) in percent (two decimals) — the apcopay formula authorized / (authorized + declined + failed): PENDING and UNKNOWN_PENDING_SYNC are still in flight and stay out of the denominator; payins counts the approved payments (research §4.5) and the Deposits card carries its own EUR line (deposits_eur, AUTHORIZED | CAPTURED only — money confirmed IN, not yet refunded, with previous_deposits_eur as the delta basis); payment_method_distribution groups the window payments per payment_method_type (count only).
daily groups by UTC calendar day and per currency; by_state is always the full state registry (zero-filled) so the portal renders a deterministic grid. Read-only aggregate over the existing tables — no new schema.
Auth model (MVP): the same admin Bearer key as the operator surfaces — the merchant id comes from the path and is resolved strictly inside the authenticated tenant; a foreign or unknown merchant is a plain 404.
pass from+to (both, YYYY-MM-DD, inclusive) for a custom period — the response echoes window: "custom" and window_days as the inclusive day count.
| id required | string Merchant id ( |
| window | string Default: "7d" enum: "7d" "30d" Trailing window of the aggregates. Ignored when |
| from | string <date> custom range start (YYYY-MM-DD, inclusive, UTC). Requires |
| to | string <date> custom range end (YYYY-MM-DD, inclusive, UTC). Requires |
| merchant_id required | string |
| window required | string enum: "7d" "30d" Echoed window value. |
| window_days required | integer enum: 7 30 Window length in days. |
| turnover_eur required | integer Turnover EUR: |
| previous_turnover_eur required | integer Same turnover aggregate over the PREVIOUS period of the same length ( |
| approval_ratio required | number <double> Approved / (approved + FAILED) payments of the window, percent 0-100, two decimals — the apcopay formula |
| transaction_count required | integer All payments of the window (any state). |
| approved_count required | integer Approved-family payments of the window (the ratio numerator). |
| failed_count required | integer FAILED payments of the window (the ratio decline side). |
| payins required | integer Pay Ins: count of |
| deposits_eur required | integer Deposits €-line: |
| previous_deposits_eur required | integer Deposits €-line of the PREVIOUS period (the delta basis of the Deposits card). |
| refunds_count required | integer Credits/Refunds: count of SUCCEEDED refunds (any currency). |
| refunds_eur required | integer Credits/Refunds: sum of SUCCEEDED refunds in EUR, minor units. |
required | Array of objects (MerchantStateCount) Full state registry of the window, zero-filled. |
required | Array of objects (MerchantCurrencyAmount) Turnover volume per currency (minor units) — the honest companion of the EUR-only turnover card. |
required | Array of objects (MerchantPaymentMethodShare) Payment Option Distribution of the window (per |
required | Array of objects (MerchantDailySummary) Daily rollup of the window (UTC days, per currency) — the Reconciliation view. |
| generated_at required | string RFC 3339 moment the snapshot was computed. |
{- "merchant_id": "mer_01900000-0000-7000-8000-00000000000a",
- "window": "7d",
- "window_days": 7,
- "turnover_eur": 3050,
- "previous_turnover_eur": 0,
- "approval_ratio": 75,
- "transaction_count": 4,
- "approved_count": 3,
- "failed_count": 1,
- "payins": 3,
- "deposits_eur": 3050,
- "previous_deposits_eur": 0,
- "refunds_count": 0,
- "refunds_eur": 0,
- "by_state": [
- {
- "state": "PENDING",
- "count": 0
}
], - "turnover_by_currency": [
- {
- "currency": "EUR",
- "amount_minor": 0
}
], - "payment_method_distribution": [
- {
- "method": "card",
- "count": 0,
- "volume_minor": 0
}
], - "daily": [
- {
- "date": "2025-09-24",
- "currency": "EUR",
- "count": 0,
- "total_amount_minor": 0,
- "by_state": [
- {
- "state": "PENDING",
- "amount_minor": 0
}
]
}
], - "generated_at": "string"
}The Transactions grid of the merchant portal (apcopay-portal layout): the merchant's payments, newest first (created_at DESC, id as the deterministic tie-break), with an optional status filter and limit/offset pagination. total counts the payments matching the filter independently of the page.
Summary projection with the apcopay columns: payment id, status, amount (minor units), currency, created_at, the latest attempt's provider + PSP reference, the payment option, the masked card metadata, the best-effort merchant context (order_reference from the request metadata, email from the request customer). No attempt journal — the full detail is the dedicated details read.
Same admin Bearer (single-principal MVP); a foreign or unknown merchant is a plain 404.
| id required | string Merchant id ( |
| status | string enum: "PENDING" "AUTHORIZED" "CAPTURED" "PARTIALLY_REFUNDED" "REFUNDED" "FAILED" "UNKNOWN_PENDING_SYNC" Optional payment-status filter (the canonical registry). |
| limit | integer [ 1 .. 200 ] Default: 50 Page size (clamped to 200). |
| offset | integer >= 0 Default: 0 Page offset (0-based). |
| q | string <= 100 characters free-text search (contains match, ILIKE) over payment_id, PSP transaction reference, order_reference and email; at most 100 characters (else |
| merchant_id required | string |
| status | string enum: "PENDING" "AUTHORIZED" "CAPTURED" "PARTIALLY_REFUNDED" "REFUNDED" "FAILED" "UNKNOWN_PENDING_SYNC" Echoed status filter (absent when unfiltered). |
| total required | integer Total payments matching the filter, independent of the page. |
| limit required | integer |
| offset required | integer |
| count required | integer Rows on this page. |
required | Array of objects (MerchantPaymentSummary) |
{- "merchant_id": "string",
- "status": "PENDING",
- "total": 0,
- "limit": 0,
- "offset": 0,
- "count": 0,
- "payments": [
- {
- "payment_id": "pay_01900000-0000-7000-8000-00000000000a",
- "status": "PENDING",
- "amount": 1050,
- "currency": "EUR",
- "created_at": "string",
- "provider": "payadmit",
- "psp_reference": "mock-txn",
- "error_code": "do_not_honor",
- "payment_method_type": "card",
- "card": {
- "brand": "VISA",
- "last4": "4242",
- "exp_month": "08",
- "exp_year": "2028"
}, - "order_reference": "string",
- "email": "string"
}
]
}The Transaction Details view of the merchant portal (apcopay-portal layout): the grid summary plus the PSP details, the related refunds and the merchant's free-form request objects (metadata, customer — the expandable JSON of the details page). The Events timeline of the UI composes attempts (this response) with the webhook deliveries of GET /v1/merchants/{id}/webhooks/deliveries?payment_id=….
Strictly tenant+merchant scoped: a payment of another merchant (or an unknown id) is a plain 404. Same admin Bearer.
| id required | string Merchant id ( |
| payment_id required | string Payment id ( |
| merchant_id required | string |
| payment_id required | string |
| status required | string enum: "PENDING" "AUTHORIZED" "CAPTURED" "PARTIALLY_REFUNDED" "REFUNDED" "FAILED" "UNKNOWN_PENDING_SYNC" |
| amount required | integer Minor units of |
| currency required | string |
| capture_method required | string enum: "automatic" "manual" |
| payment_method_type required | string |
| provider | string Latest attempt's connector code; absent without attempts. |
| psp_reference | string Latest attempt's PSP transaction reference; absent without attempts. |
| error_code | string Latest attempt's PSP decline/response code (the verbatim Error Code of the Transactions grid); absent when the attempt carries none. |
object (MerchantCardMeta) Masked instrument metadata of a payment. | |
| order_reference | string Best-effort merchant context from the request metadata. |
string Best-effort player contact from the request customer. | |
| correlation_id required | string Correlation id shared with the PA trace of the payment creation. |
| metadata | object The merchant's free-form request metadata (verbatim). |
| customer | object The merchant's free-form customer object (verbatim). |
| created_at required | string RFC 3339. |
| updated_at required | string RFC 3339. |
required | Array of objects (MerchantAttemptDetail) Attempt journal, oldest first (the Events timeline + PSP details). |
required | Array of objects (MerchantRefundSummary) Related refunds, oldest first (Related Transactions). |
{- "merchant_id": "string",
- "payment_id": "pay_01900000-0000-7000-8000-00000000000a",
- "status": "PENDING",
- "amount": 1050,
- "currency": "EUR",
- "capture_method": "automatic",
- "payment_method_type": "card",
- "provider": "payadmit",
- "psp_reference": "string",
- "error_code": "string",
- "card": {
- "brand": "VISA",
- "last4": "4242",
- "exp_month": "08",
- "exp_year": "2028"
}, - "order_reference": "string",
- "email": "string",
- "correlation_id": "string",
- "metadata": { },
- "customer": { },
- "created_at": "string",
- "updated_at": "string",
- "attempts": [
- {
- "seq": 0,
- "provider": "payadmit",
- "mid": "mid_portal",
- "status": "PENDING",
- "provider_status": "string",
- "provider_code": "string",
- "provider_transaction_id": "string",
- "started_at": "string",
- "finished_at": "string"
}
], - "refunds": [
- {
- "refund_id": "ref_01900000-0000-7000-8000-00000000000a",
- "amount": 500,
- "currency": "EUR",
- "status": "PENDING",
- "created_at": "string"
}
]
}The Transaction Report export: the same filter (status) as the payments list, delivered as a CSV attachment (Content-Type: text/csv; charset=utf-8, Content-Disposition: attachment). Columns: payment_id, status, amount_minor, currency, capture_method, payment_method_type, provider, psp_reference, card_brand, card_last4, card_expiry, order_reference, email, created_at, updated_at.
columns= selects groups and/or individual columns (Choose Columns).
the export is a STREAMING body walking keyset pages of 1000 rows (newest first) with a flat memory profile — the former bounded 10k snapshot is gone; any row count exports. A store error mid-stream aborts the body. Same admin Bearer; a foreign or unknown merchant is a plain 404.
| id required | string Merchant id ( |
| status | string enum: "PENDING" "AUTHORIZED" "CAPTURED" "PARTIALLY_REFUNDED" "REFUNDED" "FAILED" "UNKNOWN_PENDING_SYNC" Optional payment-status filter (the canonical registry). |
| columns | string Example: columns=transaction,bank comma-separated column keys and/or column-group keys ( |
payment_id,status,amount_minor,currency,capture_method,payment_method_type,provider,psp_reference,card_brand,card_last4,card_expiry,order_reference,email,created_at,updated_at pay_01900000-0000-7000-8000-00000000000a,AUTHORIZED,1050,EUR,automatic,card,payadmit,mock-txn,do_not_honor,VISA,4242,08/2028,order-77,player@example.com,2025-09-24T10:00:00Z,2025-09-24T10:00:05Z
The webhook delivery health of the merchant (portal Integration tab): the most recent deliveries of ALL payments of the merchant, newest first, with an optional payment_id filter. Same projection discipline as GET /v1/webhooks/deliveries — the stored event payload (and with it every secret) is never projected into reads.
Delivery is at-least-once: PENDING rows are retried with backoff and dead-letter as DEAD; DELIVERED means the receiver acknowledged with a 2xx. The receiver verifies the PA-Webhook-Signature HMAC on its side — this log carries the PA-side delivery health (lifecycle state, attempt counter, last error class), not the receiver's verification result. Same admin Bearer; a foreign or unknown merchant is a plain 404.
| id required | string Merchant id ( |
| payment_id | string Optional payment id ( |
| limit | integer [ 1 .. 100 ] Default: 20 Maximum number of deliveries (clamped to 100). |
| merchant_id required | string |
| payment_id | string Echoed payment filter (absent when unfiltered). |
| count required | integer Rows on this page. |
required | Array of objects (MerchantWebhookDeliveryResponse) |
{- "merchant_id": "string",
- "payment_id": "string",
- "count": 0,
- "deliveries": [
- {
- "event_id": "string",
- "event_type": "payment.authorized",
- "payment_id": "pay_01900000-0000-7000-8000-00000000000a",
- "status": "PENDING",
- "attempts": 0,
- "last_error": "string",
- "created_at": "string",
- "delivered_at": "string"
}
]
}One tenant-scoped read with everything the handover promised: the access triple — api_key (the SAME bearer of THIS request echoed back: in the single-principal MVP the tenant API key IS the admin key the merchant already holds), merchant_id and the production/sandbox base URLs — plus the webhook configuration and the routing data. webhook_url is the registered endpoint (null when none); webhook_secret_status is issued or never_shown (no webhook registered). providers lists the linked PSPs (connector + MID, currency scope, connector lifecycle status) and test_cards carries the documented sandbox terminal fixtures. Audited as selfprofile.read with masked details (merchant id only).
| id required | string Merchant id ( |
| merchant_id required | string |
| name required | string |
| api_key required | string The bearer key of THIS request (single-principal MVP echo). Never logged, never persisted by PA. |
required | object (MerchantBaseUrls) |
| webhook_url | string or null Registered webhook endpoint; |
| webhook_secret_status required | string (WebhookSecretStatus) enum: "issued" "never_shown" once-semantics marker: |
required | Array of objects (MerchantSelfProfileProvider) |
required | object (MerchantTestCards) |
{- "merchant_id": "string",
- "name": "string",
- "api_key": "string",
- "base_urls": {
- "production": "string",
- "sandbox": "string"
}, - "webhook_url": "string",
- "webhook_secret_status": "issued",
- "providers": [
- {
- "connector_code": "string",
- "mid_label": "string",
- "currencies": [
- "string"
], - "status": "string"
}
], - "test_cards": {
- "apcopay": {
- "number": "string",
- "expiry": "string",
- "holder": "string"
}, - "payadmit": {
- "number": "string",
- "expiry": "string",
- "cvc": "string"
}
}
}Changes the registered webhook endpoint URL WITHOUT rotating the signing secret: the active secret and the grace slot stay byte-identical, so the receiver keeps verifying with the key it already holds. HTTPS-only validation — plain http, garbage or overlong values are rejected; requires an EXISTING webhook registration (URL and secret are a pair). Audited as merchant.update_webhook_url (details carry the new URL, never a secret).
| id required | string Merchant id ( |
| url required | string <= 2048 characters New HTTPS endpoint of the casino backend receiving signed events (plain http is not accepted for self-service). |
| merchant_id required | string |
| webhook_url required | string The new endpoint; the signing secret is unchanged. |
{- "url": "string"
}{- "merchant_id": "string",
- "webhook_url": "string"
}Returns 200 while the server process is alive. Deliberately performs NO database check: restarting the process cannot heal PostgreSQL, so liveness stays independent of it. Unauthenticated.
| status required | string value: "ok" |
{- "status": "ok"
}Returns 200 while PostgreSQL answers a probe query (SELECT 1, 3s timeout), 503 when it does not. The orchestrator removes an unready instance from traffic rotation WITHOUT restarting it; readiness self-heals when the database recovers. Unauthenticated.
| status required | string enum: "ready" "unavailable" |
required | object |
{- "status": "ready",
- "checks": {
- "database": "up"
}
}The real Locker capture surface of the PA Secure Fields SDK: the sandboxed SDK iframe posts PAN/CVV DIRECTLY here and receives a single-use vault token. PA forwards the card bundle to the configured capture client (fork Locker proxy) and holds the values only in flight — nothing card-shaped is logged, persisted or echoed.
Auth: the PUBLIC publishable key (X-Publishable-Key: pk_..., body publishable_key fallback) — derived per merchant at onboarding, re-displayed by the merchant APIs. It binds the capture to a merchant but carries no bearer power.
Load control: hard per-key sliding window (PA_SDK_CAPTURE_RPM, default 30/min) + per-IP window; over-limit answers 429 rate_limited + Retry-After before any card data is processed. Origin allowlist (PA_SDK_CAPTURE_ALLOWED_ORIGINS) applies when configured. No Idempotency-Key: every call mints a FRESH vault token (each token is consumed exactly once by the instrument registration).
| card_number required | string PAN, digits only (12-19, no separators, Luhn-checked server-side). PCI: exists only in flight — never logged, persisted or echoed. |
| cvc required | string CVC, 3-4 digits. PCI: exists only in flight. |
| expiry_month required | string Expiry month, |
| expiry_year required | string Expiry year, 4 digits ( |
| card_holder_name | string or null Optional cardholder name (max 128 chars), forwarded to the capture surface. |
| publishable_key | string or null Optional fallback transport of the publishable key (header |
| vault_token required | string Single-use vault token of the freshly tokenized card (fork |
| brand required | string Masked card brand from the capture surface ( |
| last4 required | string Last four digits of the PAN (masked metadata). |
| exp_month | string or null |
| exp_year | string or null |
{- "card_number": "string",
- "cvc": "string",
- "expiry_month": "string",
- "expiry_year": "string",
- "card_holder_name": "string",
- "publishable_key": "string"
}{- "vault_token": "string",
- "brand": "string",
- "last4": "string",
- "exp_month": "string",
- "exp_year": "string"
}Never authenticated (browsers send no custom headers on preflights). Answers 204 with Access-Control-Allow-* headers when the Origin is acceptable (wildcard posture when no allowlist is configured), 403 origin_not_allowed otherwise.
{- "type": "string",
- "title": "Bad Request",
- "status": 422,
- "code": "unknown_merchant",
- "message": "string",
- "payment_id": "string"
}