PA Payments API (1.0.0)

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:

  • Authentication: Authorization: Bearer <tenant_api_key> for every /v1 route (see securitySchemes.bearerAuth). Health probes and the docs portal are unauthenticated.
  • Money: integer minor units (cents) + ISO 4217 alpha-3 currency.
  • Identifiers: type-safe prefixed UUIDs (pay_…, mer_…, pin_…, pout_…, tn_…, key_…).
  • Idempotency: mutating requests take an 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.
  • Errors: RFC 7807 application/problem+json with stable PA code values; PSP error codes are never exposed.
  • Rate limiting: every authenticated request consumes a sliding-window slot (default 100 rpm per tenant+endpoint); over-limit returns 429 rate_limited with Retry-After.
  • Interactive reference: 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.
  • Guides: the same /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.

Handover Package

Handover Package — передача казино-мерчанту (PA)

Приветствуем! Вы получили доступ к платформе платежей 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.


1. Ключи доступа — шаблон (заполняет Оператор PA)

⚠️ Секреты выдаются ровно один раз. Значения передаются по защищённому каналу и показываются единожды — сразу сохраните их в свой 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)

2. Что вы получаете

Артефакт Где получить Назначение
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 модель выплат, статус поверхности

3. Ваша интеграция — 6 шагов

Нумерация ниже — маршрут казино «от виджета до go-live». В чек-листе шаги детализированы своей нумерацией К1–К6; в скобках — куда смотреть за деталями.

К1. Подключить SDK iframe (frontend) — [чек-лист §5/К1]

<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).

К2. Поднять webhook receiver (backend) — [чек-лист §5/К4]

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 дней) верифицируйте оба секрета.

К3. Sandbox-платёж (backend) — [чек-лист §5/К2–К3]

Два вызова с 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.

К4. Redirect — 3DS / hosted page PSP — [чек-лист §5/К5]

Если ответ содержит next_action: {"type": "redirect", "redirect_url": "…", "method": "GET"} — переведите игрока по redirect_url. Возврат игрока на сайт — не результат: финальный статус приходит webhook'ом (К2) и/или через GET /v1/payments/{id}.

К5. Возвраты (refunds) —

POST /v1/refunds
Idempotency-Key: <uuid>
{"payment_id": "pay_…", "amount": 1050, "reason": "requested_by_customer"}

Частичные возвраты суммируются с контролем не-превышения AUTHORIZED-суммы; статусы PARTIALLY_REFUNDED → REFUNDED приходят webhook'ом (событие payment.refunded, К2). Полный цикл возвратов подтверждён acceptance-прогоном.

К6. Go-live — [чек-лист §6]

Запросите у Оператора production base URL и production ключи (tenant API key + whs_-секрет production MID) и пройдите go-live чек-лист §6 (7 пунктов: полный цикл, HMAC, идемпотентность, PCI-гигиена, ротации ключей, production MID ENABLED, мониторинг).

4. Правила безопасности (обязательные)

  1. PCI: PAN/CVV вводятся только в SDK iframe и уходят напрямую в capture-поверхность; ваш backend и PA видят только vault_token/pin_ и маскированные метаданные. PAN/CVV — никогда в логи, БД, аудит (acceptance: 0 PAN).
  2. HMAC-верификация обязательна: PA-Webhook-Signature: v1=<hex(HMAC-SHA256(raw_body, whs_секрет))> над сырым телом, сравнение timing-safe; без валидной подписи — не обрабатывать (401). Дедуп по PA-Event-Id (= event_id payload, UUIDv7).
  3. Секреты — не в git, не в логах, не в URL; хранение — secret store. Ротация: tenant-ключ POST /v1/tenants/{id}/rotate-key (новый сразу, старый 24 ч grace), webhook-секрет POST /v1/merchants/{id}/rotate-webhook-secret (grace 7 дней, dual-verify).
  4. Идемпотентность обязательна: Idempotency-Key: <uuid> на каждый POST; повтор с тем же ключом возвращает сохранённый результат (Idempotent-Replayed: true), тот же ключ с другим телом → 422 idempotency_key_reuse. Webhook-replay по PA-Event-Id не должен дублировать зачисление игроку.

5. Ограничения текущего релиза

Ограничение Суть Когда снимается
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

6. TL;DR (English)

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.

Приложение — полные ссылки

  • Глоссарий терминов (PA, PSP, MID, PURC, S2S, HPP, 3DS, PCI DSS, vault token, webhook…): Термінологія — что означает каждый термин для казино.
  • Модели интеграции и PCI DSS scope: Integration models — SDK iframe (рекомендовано) / hosted payment page (Phase 2) / почему S2S raw card не предоставляется.
  • Выплаты: Payouts — Phase 2 — модель approval-gate и статус поверхности.
  • Чек-лист онбординга казино (RU Часть I + EN Part II): Onboarding Checklist — детальные шаги К1–К6 / C1–C6, предусловия §2, go-live §6, эскалация §7.
  • API-доки: Payments API (RU) · SDK + Webhooks (RU) · Field Reference · OpenAPI: /docs/openapi.json — референс /docs (Redocly) и Swagger UI /docs/swagger-ui на вашем base URL (раздел 1).
  • Live-примеры и тестовые карты: выдаёт Оператор вместе с SDK-пакетом (раздел «Что вы получаете»); тестовые карты для sandbox — 4111 1111 1111 1111 (12/30, CVC 123), 5555 5555 5555 4444 (MC), 4000 0000 0000 0002 (3DS).

Handover Package — передача казино-мерчанту (PA)

Приветствуем! Вы получили доступ к платформе платежей 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.


1. Ключи доступа — шаблон (заполняет Оператор PA)

⚠️ Секреты выдаются ровно один раз. Значения передаются по защищённому каналу и показываются единожды — сразу сохраните их в свой 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)

2. Что вы получаете

Артефакт Где получить Назначение
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 модель выплат, статус поверхности

3. Ваша интеграция — 6 шагов

Нумерация ниже — маршрут казино «от виджета до go-live». В чек-листе шаги детализированы своей нумерацией К1–К6; в скобках — куда смотреть за деталями.

К1. Подключить SDK iframe (frontend) — [чек-лист §5/К1]

<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).

К2. Поднять webhook receiver (backend) — [чек-лист §5/К4]

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 дней) верифицируйте оба секрета.

К3. Sandbox-платёж (backend) — [чек-лист §5/К2–К3]

Два вызова с 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.

К4. Redirect — 3DS / hosted page PSP — [чек-лист §5/К5]

Если ответ содержит next_action: {"type": "redirect", "redirect_url": "…", "method": "GET"} — переведите игрока по redirect_url. Возврат игрока на сайт — не результат: финальный статус приходит webhook'ом (К2) и/или через GET /v1/payments/{id}.

К5. Возвраты (refunds) —

POST /v1/refunds
Idempotency-Key: <uuid>
{"payment_id": "pay_…", "amount": 1050, "reason": "requested_by_customer"}

Частичные возвраты суммируются с контролем не-превышения AUTHORIZED-суммы; статусы PARTIALLY_REFUNDED → REFUNDED приходят webhook'ом (событие payment.refunded, К2). Полный цикл возвратов подтверждён acceptance-прогоном.

К6. Go-live — [чек-лист §6]

Запросите у Оператора production base URL и production ключи (tenant API key + whs_-секрет production MID) и пройдите go-live чек-лист §6 (7 пунктов: полный цикл, HMAC, идемпотентность, PCI-гигиена, ротации ключей, production MID ENABLED, мониторинг).

4. Правила безопасности (обязательные)

  1. PCI: PAN/CVV вводятся только в SDK iframe и уходят напрямую в capture-поверхность; ваш backend и PA видят только vault_token/pin_ и маскированные метаданные. PAN/CVV — никогда в логи, БД, аудит (acceptance: 0 PAN).
  2. HMAC-верификация обязательна: PA-Webhook-Signature: v1=<hex(HMAC-SHA256(raw_body, whs_секрет))> над сырым телом, сравнение timing-safe; без валидной подписи — не обрабатывать (401). Дедуп по PA-Event-Id (= event_id payload, UUIDv7).
  3. Секреты — не в git, не в логах, не в URL; хранение — secret store. Ротация: tenant-ключ POST /v1/tenants/{id}/rotate-key (новый сразу, старый 24 ч grace), webhook-секрет POST /v1/merchants/{id}/rotate-webhook-secret (grace 7 дней, dual-verify).
  4. Идемпотентность обязательна: Idempotency-Key: <uuid> на каждый POST; повтор с тем же ключом возвращает сохранённый результат (Idempotent-Replayed: true), тот же ключ с другим телом → 422 idempotency_key_reuse. Webhook-replay по PA-Event-Id не должен дублировать зачисление игроку.

5. Ограничения текущего релиза

Ограничение Суть Когда снимается
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

6. TL;DR (English)

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.

Приложение — полные ссылки

  • Глоссарий терминов (PA, PSP, MID, PURC, S2S, HPP, 3DS, PCI DSS, vault token, webhook…): Термінологія — что означает каждый термин для казино.
  • Модели интеграции и PCI DSS scope: Integration models — SDK iframe (рекомендовано) / hosted payment page (Phase 2) / почему S2S raw card не предоставляется.
  • Выплаты: Payouts — Phase 2 — модель approval-gate и статус поверхности.
  • Чек-лист онбординга казино (RU Часть I + EN Part II): Onboarding Checklist — детальные шаги К1–К6 / C1–C6, предусловия §2, go-live §6, эскалация §7.
  • API-доки: Payments API (RU) · SDK + Webhooks (RU) · Field Reference · OpenAPI: /docs/openapi.json — референс /docs (Redocly) и Swagger UI /docs/swagger-ui на вашем base URL (раздел 1).
  • Live-примеры и тестовые карты: выдаёт Оператор вместе с SDK-пакетом (раздел «Что вы получаете»); тестовые карты для sandbox — 4111 1111 1111 1111 (12/30, CVC 123), 5555 5555 5555 4444 (MC), 4000 0000 0000 0002 (3DS).

Onboarding Checklist

Чек-лист онбординга казино — Casino Onboarding Checklist

Одна страница (RU + EN). Роли: Оператор PA / Казино. Связанные гайды: SDK + Webhooks, SDK README, Field Reference, Handover Package, Термінологія, Integration models.

English version: Part II ниже.


Часть I — Русский

1. Назначение и роли

Одна страница, чтобы провести казино от контракта до 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.

2. Pre-requisites (предусловия)

Оператор (до начала):

  • tenant создан (bootstrap/seed); tenant API key доступен (raw-значение показывается один раз);
  • PSP-аккаунт (payadmit | apcopay) заведён на стороне PA → создан MCA → известен credentials_ref; секреты PSP PA не хранит;
  • доступ к sandbox PA (base URL выше) и к GET /v1/audit (аудит-след своих операций).

Казино (до начала):

  • HTTPS endpoint webhook-receiver'а (в dev разрешён http; в production — обязателен HTTPS);
  • backend, способный к 2–3 вызовам PA API и приёму подписанных webhook'ов;
  • страница депозита (frontend) для подключения SDK;
  • тестовые карты PSP-sandbox (см. О6).

3. Дорожная карта онбординга (две колонки: Оператор PA + Казино)

Этап Оператор 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

4. Шаги оператора (наша сторона, admin Bearer)

Все вызовы аутентифицируются Bearer-ключом тенанта:

Authorization: Bearer <tenant_api_key>

О1. Создать профиль казино → merchant_id

POST /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).

О2. Привязать PSP и MID → 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)).

О3. Добавить Route Target (маршрут) → 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.

О4. Зарегистрировать webhook казино → секрет 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 на стороне казино).

О5. Выдать казино пакет интеграции

  • tenant API key (создаётся при tenant creation); ротация — POST /v1/tenants/{id}/rotate-key (новый ключ валиден немедленно, старый живёт 24 ч — grace period; raw показывается один раз);
  • SDK-пакет: pa-sdk/secure-fields.js + pa-sdk/secure-fields-frame.html (drop-in, без сборки; frame размещается на origin PA) + значение captureUrl для SDK;
  • документация:,, pa-sdk/README.md;
  • тестовые карты PSP-sandbox (см. О6).

О6. Sandbox сопровождение → production MID → go-live

  • убедиться в полном цикле на sandbox: платёж доходит до финального успешного статуса (AUTHORIZED/CAPTURED), webhook доставлен (2xx) — журнал: GET /v1/webhooks/deliveries?payment_id=pay_…;
  • PayAdmit sandbox ведёт карты через hosted HPP — финал завершается в браузере (hosted HPP: финал завершается в браузере); тестовые карты: 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_…;
  • после успешного smoke: production MID (ENABLED, наш оператор) → go-live (чек-лист §6).

5. Шаги казино (их разработчики)

К1. Подключить secure-fields.js (frontend)

<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>

Инварианты безопасности (не нарушать):

  1. iframe создаётся с sandbox="allow-scripts allow-forms" — БЕЗ allow-same-origin (opaque origin: casino JS не имеет DOM-доступа к значениям полей);
  2. PAN/CVV вводятся только в iframe и уходят напрямую в capture-поверхность (vault); PA видит только vault token, casino backend PAN не видит вовсе;
  3. обмен — postMessage с проверкой origin в обе стороны (namespace pa-secure-fields);
  4. vault_token — одноразовый credential: не в URL, не в логи, не в localStorage; регистрируется ровно один раз (409 vault_token_already_registered при повторе).

К2. Обменять vault token на инструмент (backend)

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.

К3. Создать платёж (backend)

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, дождитесь финала;
  • повтор POST с тем же Idempotency-Key не создаёт новый платёж — возвращает сохранённый результат.

К4. Webhook receiver (backend) — обязательный контур

Регистрацию 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 не доставляются);
  • at-least-once: retry по лестнице 1м/5м/30м/2h, после 5 неуспешных попыток — DEAD (журнал доставок у Оператора: GET /v1/webhooks/deliveries?payment_id=pay_…); отвечайте 2xx быстро (таймаут PA — 5 с по умолчанию);
  • ротация секрета (Оператор): в течение grace-окна (по умолчанию 7 дней) верифицируйте оба секрета (dual-verify: активный + предыдущий до old_secret_expires_at).

К5. next_action redirect (hosted PSP)

Если 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} до финала).

К6. Go-live

Запросить у Оператора production base URL и production ключи (tenant API key + whs_-секрет production MID; ротации — О5/О4), пройти чек-лист §6.

6. Go-live чеклист

Отметка ответственного: [О] — Оператор PA, [К] — Казино.

  • [О/К] Sandbox-платёж доведён до финального успешного статуса (AUTHORIZED/CAPTURED) — полный цикл
  • [К] Верификация PA-Webhook-Signature (HMAC-SHA256 по сырому телу) работает на receiver'е — все 4 типа событий: payment.authorized / payment.captured / payment.refunded / payment.failed
  • [К] Идемпотентность: replay Idempotency-Key возвращает сохранённый результат; тот же ключ с другим телом → 422 idempotency_key_reuse; webhook-replay по PA-Event-Id не дублирует зачисление
  • [К] PCI-гигиена: PAN/CVV отсутствуют в логах казино (SDK iframe — PAN уходит только в capture-поверхность); в логах только pin_/pay_ и маскированные метаданные
  • [О] Production ключи: ротация tenant key (POST /v1/tenants/{id}/rotate-key) и webhook-секрета (POST /v1/merchants/{id}/rotate-webhook-secret); raw-значения переданы в secret store казино
  • [О] Production MID ENABLED (наш оператор)
  • [О/К] Мониторинг: uptime receiver'а (казино), контакт-лист эскалации заполнен (§7), журнал доставок PA доступен оператору

7. Эскалация

Уровень Кому Когда
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).

8. Источники

  • Handover Package — ключи, артефакты, 6 шагов интеграции;
  • SDK + Webhooks — SDK-контракт, /v1/payment-instruments, webhook-контракт;
  • Field Reference — поля POST /v1/payments, сводка ошибок;
  • SDK README — интеграция казино (frontend + backend + smoke demo);
  • Термінологія — глоссарий; Integration models — модели и PCI scope;
  • машина состояний платежа: AUTHORIZED / CAPTURED / PENDING / FAILED / UNKNOWN_PENDING_SYNC.

Part II — English version

1. Purpose and roles

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.

2. Prerequisites

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).

3. Onboarding track (two columns)

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

4. Operator steps (our side, admin Bearer)

All calls are authenticated with the tenant Bearer key. Steps mirror §4 of Part I — same payloads and responses:

  • O1 POST /v1/merchants — casino profile {name, country, currencies[], legal_entity} → 201 with merchant_id (mer_…); audit create_merchant; verify via GET /v1/merchants/{id}.
  • O2 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.
  • O3 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.
  • O4 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).
  • O5 Hand over the integration package: tenant API key (issued at tenant creation; rotation 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.
  • O6 Sandbox support until a full cycle succeeds (final AUTHORIZED/CAPTURED, webhook delivered — journal GET /v1/webhooks/deliveries?payment_id=…), then production MID (ENABLED) → go-live checklist §6.

5. Casino steps (their developers)

  • C1 — mount the SDK (frontend). Use the 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.
  • C2 — exchange the vault token (backend). 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.
  • C3 — create the payment (backend). 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.
  • C4 — webhook receiver (backend), mandatory. The Operator registers the endpoint (O4) and hands over the 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.
  • C5 — next_action redirect (hosted PSP). If 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}.
  • C6 — go-live. Request the production base URL and production keys from the Operator, then pass the checklist §6.

6. Go-live checklist

Marking: [O] — PA Operator, [C] — Casino.

  • [O/C] Sandbox payment reaches a final successful status (AUTHORIZED/CAPTURED) — full cycle
  • [C] HMAC verification works on their receiver (all 4 event types: authorized/captured/refunded/failed)
  • [C] Idempotency: replaying an Idempotency-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-credit
  • [C] PCI hygiene: no PAN/CVV in casino logs (SDK iframe — PAN goes only to the capture surface); logs contain only pin_/pay_ and masked metadata
  • [O] Production keys: tenant key rotation (POST /v1/tenants/{id}/rotate-key) and webhook secret rotation (POST /v1/merchants/{id}/rotate-webhook-secret); raw values stored in a secret store
  • [O] Production MID ENABLED (our operator)
  • [O/C] Monitoring: receiver uptime (casino), escalation contact list filled in (§7), PA delivery journal available to the operator

7. Escalation

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.

8. Sources

Same as §8 of Part I: Handover Package, SDK + Webhooks, Field Reference, SDK README, Термінологія.

Термінологія

Термінологія — глоссарий платёжной платформы PA

Справочник терминов и сокращений 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 маршрута

Связанные материалы

Integration models

Integration models — модели интеграции приёма карт

Как казино может принимать карточные депозиты через PA, чем модели отличаются и что каждая значит для вашего PCI DSS scope. Ключи и шаги интеграции — Handover Package; детали SDK — SDK + Webhooks и SDK README.

(a) Secure Fields SDK — рекомендуется, доступно сейчас

Рекомендованная модель: iframe-виджет на странице депозита.

Как это работает:

  1. Вы подключаете SDK одним script tag и создаёте контейнер виджета:
<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.

  1. PAN/CVV игрок вводит внутри iframe, размещённого на origin PA; значения полей недоступны JS казино. Данные карты уходят напрямую в capture-поверхность PA — мимо вашего backend.

  2. Ваш 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.

(b) Hosted payment page — Phase 2 roadmap

Redirect-модель «как у PayAdmit HPP»: игрок перенаправляется на хостед-страницу оплаты на стороне PA/PSP, вводит карту там и возвращается на сайт казино.

  • Механика на стороне API уже описана контрактом: next_action: {type: "redirect", redirect_url, method} — вы переводите игрока по redirect_url, финал приходит webhook'ом и/или GET /v1/payments/{id} (шаг К5 чек-листа).
  • Сама хостед-страница PA — Phase 2 roadmap: сейчас redirect-значение next_action используется для 3DS challenge и hosted page PSP, а не для полностраничной оплаты на стороне PA.
  • PCI-следствие то же, что у SDK: карта вводится вне вашей среды.

(c) S2S raw card — сознательно не предоставляется

Модель «полный server-to-server»: ваш backend принимает номер карты и CVV игрока и пересылает их в платёжный API.

PA сознательно не предоставляет такой путь. Причины — честно:

  1. Ваш PCI DSS scope становится максимальным. Как только PAN проходит через системы казино (приём, буферизация, логи приложений, backends), казино попадает в полный PCI DSS scope — самооценка SAQ D (или A-EP для «только редирект-приёма», что raw-card всё равно не даёт). Это ежегодный аудит, сегментация сети, шифрование хранимых данных и несоизмеримо больший комплаенс-бюджет.
  2. PA принимает карту только от capture-поверхности. Payments API спроектирован так, что карточные поля отсутствуют в схеме запроса целиком: платёж создаётся по 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

Связанные материалы

Payouts (Phase 2)

Payouts (Phase 2) — модель выплат

Как платформа PA устроит выплату выигрышей (payout) и почему endpoints выплат сейчас не входят в merchant-view документации. Приём депозитов — Handover Package; глоссарий — Термінологія.

Текущий статус: Phase 2

Payout-поверхность не входит в текущий merchant-релиз: endpoints /v1/payouts отсутствуют в merchant-view справочнике /docs и не гарантируются для мерчант-интеграции. В контракте платформы (GET /docs/openapi.json) схема payout-домена уже зафиксирована — это намеренная упреждающая публикация контракта, а не рабочая поверхность Phase 1.

Модель выплат (контракт платформы)

Payout в PA — это отдельный домен с собственным жизненным циклом, а не «отрицательный платёж»:

  1. Создание. Пayout создаётся по инструменту игрока (instrument_id, masked receiver metadata — PAN/CVV-полезная нагрузка отклоняется). Запрос идемпотентен — обязателен Idempotency-Key.
  2. Approval gate — шлюз операторского одобрения. Свежесозданная выплата не диспатчится автоматически: она останавливается в состоянии AWAITING_APPROVAL и ждёт решения оператора платформы (approve / decline / cancel). Это fail-closed модель: выплата без одобрения не уйдёт в PSP.
  • next_action платежного/пayout-контракта маркирует это как {type: "approval", resource, allowed_actions} — внешнему наблюдателю видно, что ресурс ждёт операторского решения.
  • Одобрение — отдельный мутирующий вызов, сам идемпотентный; повтор с тем же Idempotency-Key реплеит сохранённое решение.
  1. Dispatch. После одобрения payout диспатчится в PSP и проходит PROCESSING → COMPLETED (или FAILED_TECHNICAL / DECLINED).
  2. Feature flag. Вся payout-поверхность прикрыта fail-closed флагом payouts_enabled: пока флаг выключен, каждый payout-endpoint отвечает 404 feature_disabled. Включение флага = включение домена на стенде.

Почему payout endpoints сейчас не в merchant-view

  • Phase 1 = депозиты. Merchant-релиз сознательно ограничен приёмом карточных депозитов (SDK → инструмент → платёж → webhook → возвраты): это проверенный acceptance-цикл. Выплаты добавляются отдельным этапом с собственным пилотом и acceptance.
  • Операторский gate. Ключевое действие выплат — одобрение оператором; первичный потребитель payout-интерфейса — консоль оператора, а не merchant-API. Публичная мерчант-поверхность выплат формируется после утверждения операторского процесса.
  • Стабильность контракта. Схемы payout-домена опубликованы в версионном контракте уже сейчас, чтобы финальная Phase 2-поверхность не ломала интеграции, написанные под согласованные имена полей и статусы.

Что появится в Phase 2 (для планирования)

  • Создание выплаты по инструменту игрока: POST /v1/payouts (идемпотентно, Idempotency-Key);
  • чтение статуса выплаты: GET /v1/payouts/{id} и список с фильтрами;
  • события выплат в webhook-канале (переходы AWAITING_APPROVAL → PROCESSING → COMPLETED/DECLINED) — после включения домена;
  • лимиты и политики одобрения настраиваются Оператором на стороне платформы; казино в Phase 2 получает уведомления webhook'ами и статусные чтения.

Интегрировать выплаты до объявления Phase 2 не следует: контракт может дозреть (имена полей останутся, гарантии готовности — нет).

Связанные материалы

Payouts (Phase 2) — модель выплат

Как платформа PA устроит выплату выигрышей (payout) и почему endpoints выплат сейчас не входят в merchant-view документации. Приём депозитов — Handover Package; глоссарий — Термінологія.

Текущий статус: Phase 2

Payout-поверхность не входит в текущий merchant-релиз: endpoints /v1/payouts отсутствуют в merchant-view справочнике /docs и не гарантируются для мерчант-интеграции. В контракте платформы (GET /docs/openapi.json) схема payout-домена уже зафиксирована — это намеренная упреждающая публикация контракта, а не рабочая поверхность Phase 1.

Модель выплат (контракт платформы)

Payout в PA — это отдельный домен с собственным жизненным циклом, а не «отрицательный платёж»:

  1. Создание. Пayout создаётся по инструменту игрока (instrument_id, masked receiver metadata — PAN/CVV-полезная нагрузка отклоняется). Запрос идемпотентен — обязателен Idempotency-Key.
  2. Approval gate — шлюз операторского одобрения. Свежесозданная выплата не диспатчится автоматически: она останавливается в состоянии AWAITING_APPROVAL и ждёт решения оператора платформы (approve / decline / cancel). Это fail-closed модель: выплата без одобрения не уйдёт в PSP.
  • next_action платежного/пayout-контракта маркирует это как {type: "approval", resource, allowed_actions} — внешнему наблюдателю видно, что ресурс ждёт операторского решения.
  • Одобрение — отдельный мутирующий вызов, сам идемпотентный; повтор с тем же Idempotency-Key реплеит сохранённое решение.
  1. Dispatch. После одобрения payout диспатчится в PSP и проходит PROCESSING → COMPLETED (или FAILED_TECHNICAL / DECLINED).
  2. Feature flag. Вся payout-поверхность прикрыта fail-closed флагом payouts_enabled: пока флаг выключен, каждый payout-endpoint отвечает 404 feature_disabled. Включение флага = включение домена на стенде.

Почему payout endpoints сейчас не в merchant-view

  • Phase 1 = депозиты. Merchant-релиз сознательно ограничен приёмом карточных депозитов (SDK → инструмент → платёж → webhook → возвраты): это проверенный acceptance-цикл. Выплаты добавляются отдельным этапом с собственным пилотом и acceptance.
  • Операторский gate. Ключевое действие выплат — одобрение оператором; первичный потребитель payout-интерфейса — консоль оператора, а не merchant-API. Публичная мерчант-поверхность выплат формируется после утверждения операторского процесса.
  • Стабильность контракта. Схемы payout-домена опубликованы в версионном контракте уже сейчас, чтобы финальная Phase 2-поверхность не ломала интеграции, написанные под согласованные имена полей и статусы.

Что появится в Phase 2 (для планирования)

  • Создание выплаты по инструменту игрока: POST /v1/payouts (идемпотентно, Idempotency-Key);
  • чтение статуса выплаты: GET /v1/payouts/{id} и список с фильтрами;
  • события выплат в webhook-канале (переходы AWAITING_APPROVAL → PROCESSING → COMPLETED/DECLINED) — после включения домена;
  • лимиты и политики одобрения настраиваются Оператором на стороне платформы; казино в Phase 2 получает уведомления webhook'ами и статусные чтения.

Интегрировать выплаты до объявления Phase 2 не следует: контракт может дозреть (имена полей останутся, гарантии готовности — нет).

Связанные материалы

SDK + Webhooks

PA Secure Fields SDK + PaymentInstrument — контракт

Контракт токенизации карты и платёжных инструментов: Secure Fields SDK (browser-capture на реальном Locker реализован — POST /v1/sdk/capture, publishable key — см. §4). Связанные гайды: Field Reference (поля платежа), Integration models (модели интеграции и PCI scope), SDK README (быстрый старт), Термінологія.

1. Общая схема (PCI boundary)

┌──────────────────────────── казино (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):

  1. PAN/CVV никогда не попадают в PA — ни в app DB, ни в логи, ни в audit details, ни в error messages, ни в тела запросов PA API (схема запроса не содержит карточных полей вовсе).
  2. Единственное, что PA хранит о карте: vault token reference (явное PCI-исключение — ссылка на PAN в Locker) + маскированные метаданные из ответа vault (brand, last4, expiry). Первых цифр PAN PA не хранит: masked view — ****<last4>.
  3. Vault token — credential: не возвращается ни одним API, не пишется в логи/аудит.
  4. Инструмент принадлежит tenant + merchant: cross-merchant переиспользование запрещено (422 instrument_merchant_mismatch).
  5. Single-use:vault token регистрируется как инструмент ровно один раз (уникальный индекс (tenant_id, vault_token_ref)), инструмент потребляется первым платежом атомарно с созданием платежа (одна транзакция: crash не может оставить «потраченный» инструмент без платежа).

2. PaymentInstrument API

Аутентификация — та же, что для всех операций API: Authorization: Bearer <tenant_api_key>; rate limiting применяется (route pattern /v1/payment-instruments).

2.1 POST /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:

  1. проверяет скоуп merchant ↔ tenant (422 unknown_merchant);
  2. запрашивает у capture-поверхности только маскированные метаданные токена (span vault_retrieve_card несёт vault_token_fingerprint, не токен);
  3. создаёт PaymentInstrument (PA token pin_...) + audit event create_payment_instrument в одной транзакции (details: merchant_id, brand, exp_month/year, customer_reference, vault_token_fingerprint — БЕЗ токена и карточных данных);
  4. закрывает idempotency-ключ сохранённым результатом (201 или stored problem).

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.

2.2 GET /v1/payment-instruments/{id} — masked view

Тот же проекция, что в 201; статус отражает жизненный цикл (ACTIVE/USED/DEACTIVATED). Чужой/несуществующий id — одинаковый 404 instrument_not_found (без утечки существования). vault_token_ref в проекции отсутствует на уровне структуры (unit-тест фиксирует отсутствие поля в сериализации).

2.3 Использование в платежах

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.

2.4 Webhook казино (smoke-путь)

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):

  • at-least-once + дедуп: событие хранится ровно один раз (UNIQUE event_id); каждая попытка (в т.ч. retry) переотправляет ТЕ ЖЕ байты payload'а → одна и та же подпись (при неизменном секрете); получатель дедуплицирует по event_id;
  • **эмиссия на переход состояния **: событие уникально на переход — UNIQUE (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 сохраняется);
  • секрет не хранится в outbox: webhook_secret берётся из конфигурации merchant'а в момент доставки;
  • dead-letter: после исчерпания бюджета попыток строка получает 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 по каждому событию платежа.

Ошибка доставки НЕ влияет на результат платежа (только лог без тела события).

2.5 Ротация webhook-секрета POST /v1/merchants/{id}/rotate-webhook-secret — ротация подписывающего секрета без

остановки доставки. Тело не требуется (секрет генерирует PA).

  • Новый секрет (whs_...) генерируется сервер-сайд и возвращается ровно один раз (webhook_secret), как при регистрации; он становится активным немедленно — все доставки подписываются уже НОВЫМ секретом.
  • Заменённый секрет переезжает в grace-слот: 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.
  • Повторная регистрация endpoint'а (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-ключа.

3. Secure Fields SDK (pa-sdk/)

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 ;

Контракт безопасности:

  1. iframe создаётся с sandbox="allow-scripts allow-forms" (БЕЗ allow-same-origin) — фрейм имеет opaque origin, не имеет доступа к DOM/storage родителя, родитель не читает DOM фрейма: casino JS не имеет DOM-доступа к значениям полей (security invariant из pa-payments-api.md §2.0).
  2. Обмен — только postMessage с проверкой origin в обе стороны; namespace pa-secure-fields; токен валидируется по форме до вызова onToken.
  3. PAN/CVV уходят из фрейма напрямую в 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 не нужен).
  4. Формат возвращаемого токена: непрозрачная строка [A-Za-z0-9_-]{8,255} (dev-схема vtok_<brand>_<last4>-<uuid>, capture-поверхность — pm_...).
  5. Коды ошибок capture-шага: 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.

4. Capture-поверхность (куда уходят PAN/CVV)

Режим 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).

5. Что сознательно НЕ входит в MVP (следующие шаги)

  1. Browser-capture для реального Locker/fork (JWE в браузере или capture-proxy) — **реализовано в **: POST /v1/sdk/capture (capture-proxy на origin PA → fork Locker), publishable key, жёсткий rate limit, CORS/origin allowlist, HTTPS-only fail-fast —.
  2. Publishable key (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).
  3. Reusable instruments (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).
  4. Webhook outbox/retry/dead-letter + подписка на async-переходы (sync worker) — реализовано в (ARC-PA6-11, см. §2.4): транзакционный outbox webhook_deliveries, delivery worker с retry/backoff (1м/5м/30м/2h), dead-letter после 5 попыток, дедуп по event_id, журнал GET /v1/webhooks/deliveries.
  5. CVV-in-cascade (retrieve CVC из vault для попытки B) — Pilot Gate PG-2.

PA Secure Fields SDK + PaymentInstrument — контракт

Контракт токенизации карты и платёжных инструментов: Secure Fields SDK (browser-capture на реальном Locker реализован — POST /v1/sdk/capture, publishable key — см. §4). Связанные гайды: Field Reference (поля платежа), Integration models (модели интеграции и PCI scope), SDK README (быстрый старт), Термінологія.

1. Общая схема (PCI boundary)

┌──────────────────────────── казино (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):

  1. PAN/CVV никогда не попадают в PA — ни в app DB, ни в логи, ни в audit details, ни в error messages, ни в тела запросов PA API (схема запроса не содержит карточных полей вовсе).
  2. Единственное, что PA хранит о карте: vault token reference (явное PCI-исключение — ссылка на PAN в Locker) + маскированные метаданные из ответа vault (brand, last4, expiry). Первых цифр PAN PA не хранит: masked view — ****<last4>.
  3. Vault token — credential: не возвращается ни одним API, не пишется в логи/аудит.
  4. Инструмент принадлежит tenant + merchant: cross-merchant переиспользование запрещено (422 instrument_merchant_mismatch).
  5. Single-use:vault token регистрируется как инструмент ровно один раз (уникальный индекс (tenant_id, vault_token_ref)), инструмент потребляется первым платежом атомарно с созданием платежа (одна транзакция: crash не может оставить «потраченный» инструмент без платежа).

2. PaymentInstrument API

Аутентификация — та же, что для всех операций API: Authorization: Bearer <tenant_api_key>; rate limiting применяется (route pattern /v1/payment-instruments).

2.1 POST /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:

  1. проверяет скоуп merchant ↔ tenant (422 unknown_merchant);
  2. запрашивает у capture-поверхности только маскированные метаданные токена (span vault_retrieve_card несёт vault_token_fingerprint, не токен);
  3. создаёт PaymentInstrument (PA token pin_...) + audit event create_payment_instrument в одной транзакции (details: merchant_id, brand, exp_month/year, customer_reference, vault_token_fingerprint — БЕЗ токена и карточных данных);
  4. закрывает idempotency-ключ сохранённым результатом (201 или stored problem).

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.

2.2 GET /v1/payment-instruments/{id} — masked view

Тот же проекция, что в 201; статус отражает жизненный цикл (ACTIVE/USED/DEACTIVATED). Чужой/несуществующий id — одинаковый 404 instrument_not_found (без утечки существования). vault_token_ref в проекции отсутствует на уровне структуры (unit-тест фиксирует отсутствие поля в сериализации).

2.3 Использование в платежах

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.

2.4 Webhook казино (smoke-путь)

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):

  • at-least-once + дедуп: событие хранится ровно один раз (UNIQUE event_id); каждая попытка (в т.ч. retry) переотправляет ТЕ ЖЕ байты payload'а → одна и та же подпись (при неизменном секрете); получатель дедуплицирует по event_id;
  • **эмиссия на переход состояния **: событие уникально на переход — UNIQUE (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 сохраняется);
  • секрет не хранится в outbox: webhook_secret берётся из конфигурации merchant'а в момент доставки;
  • dead-letter: после исчерпания бюджета попыток строка получает 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 по каждому событию платежа.

Ошибка доставки НЕ влияет на результат платежа (только лог без тела события).

2.5 Ротация webhook-секрета POST /v1/merchants/{id}/rotate-webhook-secret — ротация подписывающего секрета без

остановки доставки. Тело не требуется (секрет генерирует PA).

  • Новый секрет (whs_...) генерируется сервер-сайд и возвращается ровно один раз (webhook_secret), как при регистрации; он становится активным немедленно — все доставки подписываются уже НОВЫМ секретом.
  • Заменённый секрет переезжает в grace-слот: 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.
  • Повторная регистрация endpoint'а (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-ключа.

3. Secure Fields SDK (pa-sdk/)

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 ;

Контракт безопасности:

  1. iframe создаётся с sandbox="allow-scripts allow-forms" (БЕЗ allow-same-origin) — фрейм имеет opaque origin, не имеет доступа к DOM/storage родителя, родитель не читает DOM фрейма: casino JS не имеет DOM-доступа к значениям полей (security invariant из pa-payments-api.md §2.0).
  2. Обмен — только postMessage с проверкой origin в обе стороны; namespace pa-secure-fields; токен валидируется по форме до вызова onToken.
  3. PAN/CVV уходят из фрейма напрямую в 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 не нужен).
  4. Формат возвращаемого токена: непрозрачная строка [A-Za-z0-9_-]{8,255} (dev-схема vtok_<brand>_<last4>-<uuid>, capture-поверхность — pm_...).
  5. Коды ошибок capture-шага: 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.

4. Capture-поверхность (куда уходят PAN/CVV)

Режим 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).

5. Что сознательно НЕ входит в MVP (следующие шаги)

  1. Browser-capture для реального Locker/fork (JWE в браузере или capture-proxy) — **реализовано в **: POST /v1/sdk/capture (capture-proxy на origin PA → fork Locker), publishable key, жёсткий rate limit, CORS/origin allowlist, HTTPS-only fail-fast —.
  2. Publishable key (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).
  3. Reusable instruments (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).
  4. Webhook outbox/retry/dead-letter + подписка на async-переходы (sync worker) — реализовано в (ARC-PA6-11, см. §2.4): транзакционный outbox webhook_deliveries, delivery worker с retry/backoff (1м/5м/30м/2h), dead-letter после 5 попыток, дедуп по event_id, журнал GET /v1/webhooks/deliveries.
  5. CVV-in-cascade (retrieve CVC из vault для попытки B) — Pilot Gate PG-2.

SDK README

PA Secure Fields SDK — интеграция для казино

Руководство по подключению приёма карточных депозитов через PA. Контракт API — гайд SDK + Webhooks; capture-контур и его security-модель управляются Оператором и мерчанту не требуются.

0. Как это работает (30 секунд)

  1. Игрок вводит карту в iframe-виджет (PA Secure Fields SDK) — значения полей недоступны JS казино.
  2. Карта уходит напрямую в capture-поверхность (fork → Locker); казино-фронтенд и casino backend получают только single-use vault token.
  3. Casino backend регистрирует инструмент: POST /v1/payment-instruments {vault_token} → получает pin_... (PA token, маскированные метаданные ****1111, brand).
  4. Casino backend создаёт платёж: POST /v1/payments {payment_instrument_id, amount, currency} → cascade → финальный статус.
  5. PA доставляет подписанный webhook на endpoint казино: payment.authorized — зачисляем депозит игроку.

PAN/CVV никогда не касаются casino backend и PA: нет ни БД-колонок, ни логов, ни аудита.

1. Что нужно казино

Компонент Что делает
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).

2. Frontend: виджет депозита

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;
  • токен живёт до регистрации инструмента; повторно зарегистрировать его нельзя (409 vault_token_already_registered);
  • не храните токен в localStorage — передавайте сразу на backend.

3. Backend: три вызова PA + webhook

Все вызовы — Authorization: Bearer <API-ключ>: merchant-scoped ключ казино (mer_sk_… — выдаёт оператор, работает только с вашим merchant_id) или tenant-ключ оператора. На POST обязателен Idempotency-Key (uuid). Ошибки — RFC 7807 (code — машинный дискриминатор).

3.1 Регистрация инструмента

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).

3.2 Создание платежа

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.

3.3 Webhook

Однократно зарегистрируйте 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 портала.

Обработка на вашей стороне (обязательно):

  1. проверить подпись: PA-Webhook-Signature: v1=<hex(HMAC-SHA256(raw_body, webhook_secret))> над сырым телом запроса;
  2. дедуп по event_id;
  3. payment.authorized / payment.captured → зачисление игроку;
  4. ответить 2xx (иначе PA посчитает доставку неуспешной).

Типы событий: payment.authorized, payment.captured, payment.refunded, payment.failed. Доставка — at-least-once: ретраи до DELIVERED, после исчерпания — DEAD; журнал доставок — вкладка Integration портала); статус платежа — источник истины через polling.

4. Onboarding казино (один раз)

Через 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 (все ключи мерчанта меняются одномоментно).

5. Локальный smoke (demo)

Тестовое казино целиком (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/:

  1. Onboard казино — merchant/provider/route-target/webhook через PA API;
  2. введите тестовую карту 4111 1111 1111 1111, 12/30, CVC 123 → Tokenize card — в demo-режиме capture-поверхности токен mintится внутри iframe (без сети);
  3. Создать платёж по инструменту → статус AUTHORIZED в логе шага 3;
  4. через ~3 с — подписанный payment.authorized в журнале шага 4.

PCI-наблюдение: в логах демо-сервера и PA нет ни PAN, ни CVC, ни vault token — только pin_..., pay_... и метаданные ****1111.

6. Тестовый режим capture ('demo') и прод

Два режима 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).

Field Reference

Field Reference — поля POST /v1/payments (справочник казино-разработчика)

Справочник полей создания платежа: типы, обязательность, ограничения и коды ошибок, которые платформа отвечает на нарушение каждого правила. Дополнение к гайду 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" }
}

1. Корневые поля

Поле Тип Обязательность Назначение и ограничения
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.

2. payment_method

Поле Значения Ограничения Ошибка при нарушении
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).

3. capture_method

Значение Семантика Комментарий
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 (без попыток исполнения).

3.1 next_action → collect_data: досбор данных игрока

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-процессором платежа;
  • клиент собирает эти поля у игрока — через Secure Fields SDK (гайд SDK + Webhooks) или последующей формой на своей стороне — и создаёт новый платёж (POST /v1/payments) либо повторяет confirm-вызов, передав запрошенные данные;
  • в текущем MVP-флоу (Apcopay S2S / PayAdmit) платформа отвечает none/redirect/wait; collect_data зарезервирован контрактом заранее — клиент интеграции должен уметь обрабатывать это значение так же, как redirect: не считать его финалом и не кредитовать игрока до финального статуса.

Пример: {"type": "collect_data", "required_fields": ["email", "phone"]} — соберите у игрока email и телефон и повторите создание платежа, передав их в блоке customer (§4).

4. customer — данные игрока

Свободная форма сохраняется целиком (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 видит контекст игрока.

5. billing_address

Проецируется в 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, …). Читается роутингом

Две независимые причины заполнять:

  1. Роутинг (country-filter): Route Target со страновым фильтром обслуживает только платежи с совпадающим billing-страновым кодом; платёж без страны страново-фильтрованный target никогда не матчит — если в приоритете остались только такие target, это 422 no_eligible_route. Валютный eligibility без страны работает, но список маршрутов может быть уже ожидаемого.
  2. PSP-биллинг: Apcopay DEPOSIT-профиль требует адрес (addressLine1, city, countryCode, postalCode) — без блока billing_address платёж уходит без billing-данных и коннектор может отвергнуть его на своей валидации.

6. risk_context / session_context

Оба блока свободной формы: сохраняются с платежём, на 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: собирайте блок на клиенте и передавайте всегда.

7. metadata и order_reference

metadata — свободная форма, сохраняется с платежём. Соглашение платформы: внешний ID заказа кладите в metadata.order_reference — тогда он появляется в мерчант-портале (колонка Order Reference списка транзакций, свободный поиск q и CSV-экспорт report.csv). Передавать его отдельным полем не нужно — отдельного поля в контракте нет.

8. Write-only поля: PaymentResponse НЕ эхоит контекст

Ответ (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).

9. Сводка ошибок по полям

Код 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.

Field Reference — поля POST /v1/payments (справочник казино-разработчика)

Справочник полей создания платежа: типы, обязательность, ограничения и коды ошибок, которые платформа отвечает на нарушение каждого правила. Дополнение к гайду 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" }
}

1. Корневые поля

Поле Тип Обязательность Назначение и ограничения
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.

2. payment_method

Поле Значения Ограничения Ошибка при нарушении
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).

3. capture_method

Значение Семантика Комментарий
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 (без попыток исполнения).

3.1 next_action → collect_data: досбор данных игрока

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-процессором платежа;
  • клиент собирает эти поля у игрока — через Secure Fields SDK (гайд SDK + Webhooks) или последующей формой на своей стороне — и создаёт новый платёж (POST /v1/payments) либо повторяет confirm-вызов, передав запрошенные данные;
  • в текущем MVP-флоу (Apcopay S2S / PayAdmit) платформа отвечает none/redirect/wait; collect_data зарезервирован контрактом заранее — клиент интеграции должен уметь обрабатывать это значение так же, как redirect: не считать его финалом и не кредитовать игрока до финального статуса.

Пример: {"type": "collect_data", "required_fields": ["email", "phone"]} — соберите у игрока email и телефон и повторите создание платежа, передав их в блоке customer (§4).

4. customer — данные игрока

Свободная форма сохраняется целиком (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 видит контекст игрока.

5. billing_address

Проецируется в 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, …). Читается роутингом

Две независимые причины заполнять:

  1. Роутинг (country-filter): Route Target со страновым фильтром обслуживает только платежи с совпадающим billing-страновым кодом; платёж без страны страново-фильтрованный target никогда не матчит — если в приоритете остались только такие target, это 422 no_eligible_route. Валютный eligibility без страны работает, но список маршрутов может быть уже ожидаемого.
  2. PSP-биллинг: Apcopay DEPOSIT-профиль требует адрес (addressLine1, city, countryCode, postalCode) — без блока billing_address платёж уходит без billing-данных и коннектор может отвергнуть его на своей валидации.

6. risk_context / session_context

Оба блока свободной формы: сохраняются с платежём, на 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: собирайте блок на клиенте и передавайте всегда.

7. metadata и order_reference

metadata — свободная форма, сохраняется с платежём. Соглашение платформы: внешний ID заказа кладите в metadata.order_reference — тогда он появляется в мерчант-портале (колонка Order Reference списка транзакций, свободный поиск q и CSV-экспорт report.csv). Передавать его отдельным полем не нужно — отдельного поля в контракте нет.

8. Write-only поля: PaymentResponse НЕ эхоит контекст

Ответ (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).

9. Сводка ошибок по полям

Код 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.

Payments

Payment lifecycle (create, read). Status machine: PENDING → AUTHORIZED → CAPTURED → PARTIALLY_REFUNDED → REFUNDED; FAILED; UNKNOWN_PENDING_SYNC.

Create a payment

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.

Authorizations:
bearerAuth
header Parameters
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. 201 for a successful creation) and body, with header Idempotent-Replayed: true (a fresh execution answers false; no new domain event is written). Same key + different body is a 422 idempotency_key_reuse; a stored deterministic 4xx problem replays as the same problem. TTL 24h.

Request Body schema: application/json
required
merchant_id
required
string

Merchant of this tenant (mer_…). A merchant outside the tenant of the Bearer key → 422 unknown_merchant.

amount
required
integer >= 1

Positive integer in minor units (cents); 0 or negative → 422 invalid_request.

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 422 no_eligible_route without starting an attempt.

object

Payment method selection. Card payments reference a PA instrument (pin_…) registered from a single-use vault token; PAN/CVV are never accepted here.

capture_method
string
Default: "automatic"
enum: "automatic" "manual"

automatic (default) — sale model: the PSP dispatch is PURC, so PA AUTHORIZED already means the provider captured the money (provider_status COMPLETED). manual — authorize-only; the capture endpoint is NOT part of the public API surface, and Route Targets are capability-filtered: a target without the manual-capture capability never serves the request (422 no_eligible_route when nothing remains; the pilot merchant routes are configured automatic only).

object

Customer context (identity/contact); risk fields belong to risk_context. Must be a JSON object (or absent/null) — any other JSON type → 422 invalid_request. FREE-FORM and WRITE-ONLY: the object is stored verbatim and projected to the PSP connector call, but the payment response NEVER echoes it (see PaymentResponse; the merchant portal Transaction Details / CSV report are the read surface).

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 browser block, which is projected into the PSP browser_info of the dispatch (the Apcopay DirectConnect PURC requires UserAgent/TimeZone; absent context → the dispatch carries no browser data).

object

Billing address; free-form and write-only (never echoed by the payment response). Two consumers: (1) the routing country filter — country_code/country scopes Route Target selection, and a payment WITHOUT a country never matches a country-filtered target (an all-filtered cascade → 422 no_eligible_route); (2) the PSP billing projection (address.line1 etc. — the Apcopay S2S DEPOSIT profile requires addressLine1, city, countryCode, postalCode).

object

Free-form client metadata (e.g. order_id); stored with the payment, WRITE-ONLY: never echoed by the payment response. Convention: the external order reference belongs in metadata.order_reference — that key surfaces in the merchant portal (Order Reference column, free-text search q, CSV report).

Responses

Response Headers
Idempotent-Replayed
string
enum: "true" "false"

false on a fresh execution, true on a replay of the stored result (present on every response of this idempotent POST).

Response Schema: application/json
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 (type discriminator): what the client should do next. none — nothing to do; wait carries retry_after_secs (synchronize later); redirect carries the hosted-page URL (3DS / hosted payment page); collect_data asks the client to gather additional data from the player; approval marks an operator action (payout gate).

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>

Request samples

Content type
application/json
{
  • "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": { }
}

Response samples

Content type
application/json
{
  • "payment_id": "pay_01900000-0000-7000-8000-00000000000a",
  • "status": "PENDING",
  • "amount": 1050,
  • "currency": "EUR",
  • "capture_method": "automatic",
  • "next_action": {
    • "type": "none"
    },
  • "attempts_summary": [
    • {
      }
    ],
  • "created_at": "2019-08-24T14:15:22Z"
}

Fetch a payment

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).

Authorizations:
bearerAuth
path Parameters
id
required
string

Payment id (pay_…).

Responses

Response Headers
Retry-After
integer

Seconds the client should wait before retrying.

Response Schema: application/json
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 (type discriminator): what the client should do next. none — nothing to do; wait carries retry_after_secs (synchronize later); redirect carries the hosted-page URL (3DS / hosted payment page); collect_data asks the client to gather additional data from the player; approval marks an operator action (payout gate).

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>

Response samples

Content type
application/json
{
  • "payment_id": "pay_01900000-0000-7000-8000-00000000000a",
  • "status": "PENDING",
  • "amount": 1050,
  • "currency": "EUR",
  • "capture_method": "automatic",
  • "next_action": {
    • "type": "none"
    },
  • "attempts_summary": [
    • {
      }
    ],
  • "created_at": "2019-08-24T14:15:22Z"
}

Refunds

Idempotent refunds of authorized/captured payments.

Create a refund

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.

Authorizations:
bearerAuth
header Parameters
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. 201 for a successful creation) and body, with header Idempotent-Replayed: true (a fresh execution answers false; no new domain event is written). Same key + different body is a 422 idempotency_key_reuse; a stored deterministic 4xx problem replays as the same problem. TTL 24h.

Request Body schema: application/json
required
payment_id
required
string
amount
required
integer >= 1

Minor units; must not exceed the remaining refundable amount.

reason
string

Responses

Response Headers
Idempotent-Replayed
string
enum: "true" "false"

false on a fresh execution, true on a replay of the stored result (present on every response of this idempotent POST).

Response Schema: application/json
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.

Request samples

Content type
application/json
{
  • "payment_id": "pay_01900000-0000-7000-8000-00000000000a",
  • "amount": 1,
  • "reason": "string"
}

Response samples

Content type
application/json
{
  • "refund_id": "string",
  • "payment_id": "string",
  • "amount": 0,
  • "currency": "string",
  • "status": "PENDING",
  • "payment_status": "AUTHORIZED"
}

Payment Instruments

Card instrument registration from single-use vault tokens (Secure Fields capture surface). No PAN/CVV ever reaches the backend API.

Register a payment instrument from a single-use vault token

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.

Authorizations:
bearerAuth
header Parameters
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. 201 for a successful creation) and body, with header Idempotent-Replayed: true (a fresh execution answers false; no new domain event is written). Same key + different body is a 422 idempotency_key_reuse; a stored deterministic 4xx problem replays as the same problem. TTL 24h.

Request Body schema: application/json
required
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).

Responses

Response Headers
Idempotent-Replayed
string
enum: "true" "false"

false on a fresh execution, true on a replay of the stored result (present on every response of this idempotent POST).

Response Schema: application/json
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.

Request samples

Content type
application/json
{
  • "merchant_id": "string",
  • "vault_token": "string",
  • "customer_reference": "string"
}

Response samples

Content type
application/json
{
  • "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"
}

Fetch a payment instrument (masked view)

Returns masked card metadata only (masked PAN ****<last4>, brand, optional expiry) and the instrument lifecycle status. No vault token is ever returned.

Authorizations:
bearerAuth
path Parameters
id
required
string

Payment instrument id (pin_…).

Responses

Response Schema: application/json
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.

Response samples

Content type
application/json
{
  • "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 Onboarding

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.

Rotate the webhook signing secret

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).

Authorizations:
bearerAuth
path Parameters
id
required
string

Merchant id (mer_…).

Responses

Response Schema: application/json
merchant_id
required
string
webhook_url
required
string
webhook_secret
required
string

The NEW active HMAC signing secret (whs_…) — shown EXACTLY ONCE, never surfaced again, never logged. From now on deliveries are signed with it immediately.

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.

Response samples

Content type
application/json
{
  • "merchant_id": "string",
  • "webhook_url": "string",
  • "webhook_secret": "string",
  • "old_secret_expires_at": "string",
  • "rotated_at": "string"
}

Rotate the merchant-scoped API key

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).

Authorizations:
bearerAuth
path Parameters
id
required
string

Merchant id (mer_…).

Responses

Response Schema: application/json
merchant_id
required
string

Merchant id (mer_…) the key is scoped to.

api_key
required
string

The raw merchant-scoped API key (mer_sk_…) — shown EXACTLY ONCE, never surfaced again, never logged; only its SHA-256 hash is persisted.

created_at
required
string

RFC 3339 issue/rotate time of the key.

Response samples

Content type
application/json
{
  • "merchant_id": "string",
  • "api_key": "string",
  • "created_at": "string"
}

Merchant Portal

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.

Merchant dashboard statistics

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.

Authorizations:
bearerAuth
path Parameters
id
required
string

Merchant id (mer_…).

query Parameters
window
string
Default: "7d"
enum: "7d" "30d"

Trailing window of the aggregates. Ignored when from+to are given.

from
string <date>

custom range start (YYYY-MM-DD, inclusive, UTC). Requires to; from < to, span at most 90 days, else 400 invalid_request.

to
string <date>

custom range end (YYYY-MM-DD, inclusive, UTC). Requires from.

Responses

Response Schema: application/json
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: AUTHORIZED | CAPTURED volume in EUR, MINOR units (cents). Money IN — refunded volume is reported as Credits/Refunds, not turnover.

previous_turnover_eur
required
integer

Same turnover aggregate over the PREVIOUS period of the same length ([now-2N, now-N)) — the 'Previously' line of the Turnover card.

approval_ratio
required
number <double>

Approved / (approved + FAILED) payments of the window, percent 0-100, two decimals — the apcopay formula authorized / (authorized + declined + failed). In-flight states (PENDING, UNKNOWN_PENDING_SYNC) stay out of the denominator; an undecided window answers 0.

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 AUTHORIZED | CAPTURED payments (successful deposits).

deposits_eur
required
integer

Deposits €-line: AUTHORIZED | CAPTURED volume in EUR, minor units — the money confirmed IN of the Deposits card.

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 payment_method_type, count only).

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.

Response samples

Content type
application/json
{
  • "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": [
    • {
      }
    ],
  • "turnover_by_currency": [
    • {
      }
    ],
  • "payment_method_distribution": [
    • {
      }
    ],
  • "daily": [
    • {
      }
    ],
  • "generated_at": "string"
}

Merchant payments list

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.

Authorizations:
bearerAuth
path Parameters
id
required
string

Merchant id (mer_…).

query Parameters
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 400 invalid_request). The total is search-scoped.

Responses

Response Schema: application/json
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)

Response samples

Content type
application/json
{
  • "merchant_id": "string",
  • "status": "PENDING",
  • "total": 0,
  • "limit": 0,
  • "offset": 0,
  • "count": 0,
  • "payments": [
    • {
      }
    ]
}

Merchant payment details

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.

Authorizations:
bearerAuth
path Parameters
id
required
string

Merchant id (mer_…).

payment_id
required
string

Payment id (pay_…).

Responses

Response Schema: application/json
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.

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.

email
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).

Response samples

Content type
application/json
{
  • "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": [
    • {
      }
    ],
  • "refunds": [
    • {
      }
    ]
}

Merchant transactions report (CSV)

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.

Authorizations:
bearerAuth
path Parameters
id
required
string

Merchant id (mer_…).

query Parameters
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 (transaction, bank, card, customer). Columns: payment_id, status, amount_minor, currency, capture_method, payment_method_type, provider, psp_reference, error_code, card_brand, card_last4, card_expiry, order_reference, email, created_at, updated_at. Omitted → the full default set. The output order is always the canonical column order. Unknown keys / an empty selection → 400 invalid_request.

Responses

Response Schema: text/csv
string

Response samples

Content type
text/csv
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

Merchant webhook delivery health

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.

Authorizations:
bearerAuth
path Parameters
id
required
string

Merchant id (mer_…).

query Parameters
payment_id
string

Optional payment id (pay_…) to narrow the view to one payment's events.

limit
integer [ 1 .. 100 ]
Default: 20

Maximum number of deliveries (clamped to 100).

Responses

Response Schema: application/json
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)

Response samples

Content type
application/json
{
  • "merchant_id": "string",
  • "payment_id": "string",
  • "count": 0,
  • "deliveries": [
    • {
      }
    ]
}

Self-service profile of the merchant

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).

Authorizations:
bearerAuth
path Parameters
id
required
string

Merchant id (mer_…).

Responses

Response Schema: application/json
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; null when none (then webhook_secret_status is never_shown).

webhook_secret_status
required
string (WebhookSecretStatus)
enum: "issued" "never_shown"

once-semantics marker: issued = a webhook (URL+secret) is registered — the secret was shown exactly once and is never returned by a read; never_shown = no webhook registered yet.

required
Array of objects (MerchantSelfProfileProvider)
required
object (MerchantTestCards)

Response samples

Content type
application/json
{
  • "merchant_id": "string",
  • "name": "string",
  • "api_key": "string",
  • "base_urls": {
    • "production": "string",
    • "sandbox": "string"
    },
  • "webhook_url": "string",
  • "webhook_secret_status": "issued",
  • "providers": [
    • {
      }
    ],
  • "test_cards": {
    • "apcopay": {
      },
    • "payadmit": {
      }
    }
}

Retarget the webhook endpoint (self-service)

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).

Authorizations:
bearerAuth
path Parameters
id
required
string

Merchant id (mer_…).

Request Body schema: application/json
required
url
required
string <= 2048 characters

New HTTPS endpoint of the casino backend receiving signed events (plain http is not accepted for self-service).

Responses

Response Schema: application/json
merchant_id
required
string
webhook_url
required
string

The new endpoint; the signing secret is unchanged.

Request samples

Content type
application/json
{
  • "url": "string"
}

Response samples

Content type
application/json
{
  • "merchant_id": "string",
  • "webhook_url": "string"
}

Ops

Unauthenticated health probes for orchestrators.

Liveness probe

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.

Responses

Response Schema: application/json
status
required
string
value: "ok"

Response samples

Content type
application/json
{
  • "status": "ok"
}

Readiness probe

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.

Responses

Response Schema: application/json
status
required
string
enum: "ready" "unavailable"
required
object

Response samples

Content type
application/json
{
  • "status": "ready",
  • "checks": {
    • "database": "up"
    }
}

Docs

This documentation portal.

API reference (branded Redocly page)

Serves the branded, self-contained Redocly reference page built from this document. Unauthenticated; safe to bookmark for integration engineers.

Responses

Response Schema: text/html
string

SDK Capture

Browser card capture — tokenize PAN/CVV into a single-use vault token

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).

Request Body schema: application/json
required
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, 01-12.

expiry_year
required
string

Expiry year, 4 digits (2030).

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 X-Publishable-Key is canonical).

Responses

Response Schema: application/json
vault_token
required
string

Single-use vault token of the freshly tokenized card (fork pm_...). The casino frontend forwards it to its backend, which registers the instrument via POST /v1/payment-instruments. Consumed exactly once.

brand
required
string

Masked card brand from the capture surface (UNKNOWN fallback).

last4
required
string

Last four digits of the PAN (masked metadata).

exp_month
string or null
exp_year
string or null

Request samples

Content type
application/json
{
  • "card_number": "string",
  • "cvc": "string",
  • "expiry_month": "string",
  • "expiry_year": "string",
  • "card_holder_name": "string",
  • "publishable_key": "string"
}

Response samples

Content type
application/json
{
  • "vault_token": "string",
  • "brand": "string",
  • "last4": "string",
  • "exp_month": "string",
  • "exp_year": "string"
}

CORS preflight of the capture endpoint

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.

Responses

Response samples

Content type
application/problem+json
{
  • "type": "string",
  • "title": "Bad Request",
  • "status": 422,
  • "code": "unknown_merchant",
  • "message": "string",
  • "payment_id": "string"
}