Unified Interface
IPaymentProvider -- a unified contract for all PSP adapters. Full spec for backend and frontend.
The orchestrator never knows which PSP it is working with -- it only calls IPaymentProvider methods. Each PSP adapter implements this interface and is responsible for data normalization, signature verification, and error handling. The orchestrator never sees raw PSP data.
TypeScript Types
// IPaymentProvider -- implement this interface for every PSP adapter
export interface IPaymentProvider {
initiateDeposit(req: DepositRequest): Promise<UnifiedResponse>
initiateWithdrawal(req: WithdrawalRequest): Promise<UnifiedResponse>
getTransactionStatus(psp_payment_id: string): Promise<UnifiedStatusResponse>
handleWebhook(payload: RawWebhookPayload): Promise<UnifiedEvent>
getSupportedMethods(geo: string, currency: string): Promise<PaymentMethod[]>
}// ── Request types ──────────────────────────────────────────────────────────────
export interface DepositRequest {
payment_id: string // idempotency key
user_id: string
brand_id: string
amount: number // USD cents, integer
currency: string // ISO 4217
method: string // e.g. "btc", "usdt_trc20"
return_url?: string
metadata?: Record<string, string>
}
export interface WithdrawalRequest {
payment_id: string // idempotency key
user_id: string
brand_id: string
amount: number // USD cents, integer
currency: string // ISO 4217
method: string
wallet_address: string
tag?: string // required for XRP, TON
}
export interface RawWebhookPayload {
body: unknown
headers: Record<string, string>
raw_body: Buffer // for HMAC verification
}
// ── Response types ─────────────────────────────────────────────────────────────
export interface UnifiedResponse {
payment_id: string
psp_payment_id: string
status: UnifiedStatus
action: UnifiedAction
redirect_url?: string | null
address?: string | null
tag?: string | null
expires_at?: string | null // ISO 8601
}
export interface UnifiedStatusResponse {
psp_payment_id: string
status: UnifiedStatus
amount_credited?: number | null // USD cents
blockchain_tx_id?:string | null
}
export interface UnifiedEvent {
event_type: UnifiedEventType
psp_payment_id: string
status: UnifiedStatus
amount_credited?: number | null // USD cents
amount_debited?: number | null // USD cents
fee_total?: number | null // USD cents
blockchain_tx_id?:string | null
}
export interface PaymentMethod {
slug: string
name: string
min_amount: number // USD cents
max_amount: number // USD cents
fee_pct?: number // 0--100
logo_url?: string
}
// ── Enums ──────────────────────────────────────────────────────────────────────
export type UnifiedStatus =
| 'INITIATED'
| 'PROCESSING'
| 'PENDING_CONFIRMATION'
| 'PENDING_PARTIAL'
| 'COMPLETED'
| 'FAILED'
| 'TIMED_OUT'
| 'CANCELLED'
export type UnifiedAction =
| 'redirect'
| 'show_address'
| 'show_qr'
export type UnifiedEventType =
| 'deposit_confirmed'
| 'deposit_processing'
| 'withdrawal_completed'
| 'withdrawal_failed'
| 'partial_payment'export class UnifiedPaymentError extends Error {
constructor(
public readonly code: UnifiedErrorCode,
public readonly message: string,
public readonly psp_raw?: unknown, // original PSP error for logging
) {
super(message)
}
}
export type UnifiedErrorCode =
| 'PSP_UNAVAILABLE' // PSP returned 5xx or timed out
| 'INVALID_METHOD' // method not supported for this geo/currency
| 'AMOUNT_BELOW_MIN' // amount < method minimum
| 'AMOUNT_ABOVE_MAX' // amount > method maximum
| 'CURRENCY_NOT_SUPPORTED' // PSP does not support this currency
| 'INVALID_WALLET_ADDRESS' // wallet address failed validation
| 'INSUFFICIENT_PSP_BALANCE' // PSP account balance too low for withdrawal
| 'TRANSACTION_NOT_FOUND' // psp_payment_id not found at PSP
| 'INVALID_SIGNATURE' // webhook HMAC verification failed
| 'UNKNOWN_EVENT_TYPE' // webhook event type not recognized
| 'MALFORMED_PAYLOAD' // webhook payload missing required fieldsMethod Contracts
initiateDeposit(req: DepositRequest)Promise<UnifiedResponse>Creates a deposit session with the PSP. Returns a redirect URL or crypto address for the player.
Input Parameters
| Field | Type | Required | Description |
|---|---|---|---|
payment_id | string | Yes | Unique idempotency key, generated by orchestrator |
user_id | string | Yes | Player identifier |
brand_id | string | Yes | Brand / tenant identifier |
amount | number | Yes | Amount in USD cents (integer) |
currency | string | Yes | ISO 4217 currency code (e.g. "USD") |
method | string | Yes | Payment method slug (e.g. "btc", "usdt_trc20") |
return_url | string | No | URL to redirect player after payment (if PSP supports) |
metadata | Record<string, string> | No | Arbitrary key-value pairs passed through to webhook events |
Output Fields
| Field | Type | Required | Description |
|---|---|---|---|
payment_id | string | Yes | Echo of the input payment_id |
psp_payment_id | string | Yes | PSP-internal transaction identifier (store for idempotency) |
status | UnifiedStatus | Yes | Always INITIATED at this stage |
action | UnifiedAction | Yes | redirect | show_address | show_qr |
redirect_url | string | null | No | Set when action = redirect |
address | string | null | No | Crypto address, set when action = show_address |
tag | string | null | No | Destination tag / memo (XRP, TON etc.) |
expires_at | string | null | No | ISO 8601 expiry timestamp (for invoice methods) |
Possible Errors
PSP_UNAVAILABLEINVALID_METHODAMOUNT_BELOW_MINAMOUNT_ABOVE_MAXCURRENCY_NOT_SUPPORTEDMust be idempotent: calling with the same payment_id twice must return the same response without creating a second PSP transaction.
If PSP requires amount in crypto, the adapter is responsible for the USD -- crypto conversion before the request.
initiateWithdrawal(req: WithdrawalRequest)Promise<UnifiedResponse>Submits a withdrawal request to the PSP. Funds are sent to the player's blockchain address.
Input Parameters
| Field | Type | Required | Description |
|---|---|---|---|
payment_id | string | Yes | Unique idempotency key |
user_id | string | Yes | Player identifier |
brand_id | string | Yes | Brand / tenant identifier |
amount | number | Yes | Amount in USD cents (integer) |
currency | string | Yes | ISO 4217 currency code |
method | string | Yes | Payment method slug |
wallet_address | string | Yes | Player's destination wallet address |
tag | string | No | Destination tag / memo (required for XRP, TON) |
Output Fields
| Field | Type | Required | Description |
|---|---|---|---|
payment_id | string | Yes | Echo of the input payment_id |
psp_payment_id | string | Yes | PSP-internal transaction identifier |
status | UnifiedStatus | Yes | INITIATED or PROCESSING depending on PSP |
Possible Errors
PSP_UNAVAILABLEINVALID_WALLET_ADDRESSAMOUNT_BELOW_MINAMOUNT_ABOVE_MAXINSUFFICIENT_PSP_BALANCEAdapter must convert USD amount to crypto before calling PSP if required.
Idempotency is critical -- duplicate calls must not send funds twice.
getTransactionStatus(psp_payment_id: string)Promise<UnifiedStatusResponse>Polls the PSP for the current transaction status. Used by the reconciliation job for stuck transactions.
Input Parameters
| Field | Type | Required | Description |
|---|---|---|---|
psp_payment_id | string | Yes | PSP-internal transaction ID returned by initiate* methods |
Output Fields
| Field | Type | Required | Description |
|---|---|---|---|
psp_payment_id | string | Yes | Echo of the input identifier |
status | UnifiedStatus | Yes | Current unified status |
amount_credited | number | null | No | Final credited amount in USD cents |
blockchain_tx_id | string | null | No | On-chain transaction hash if available |
Possible Errors
PSP_UNAVAILABLETRANSACTION_NOT_FOUNDThis method is for reconciliation only -- not for real-time UI updates. Use webhooks + status polling for UI.
handleWebhook(payload: RawWebhookPayload)Promise<UnifiedEvent>Parses and verifies a raw webhook from the PSP. Returns a normalized event the orchestrator can act on.
Input Parameters
| Field | Type | Required | Description |
|---|---|---|---|
body | unknown | Yes | Raw parsed JSON body of the webhook request |
headers | Record<string, string> | Yes | HTTP headers (needed for signature verification) |
raw_body | Buffer | Yes | Raw unmodified request body bytes for HMAC verification |
Output Fields
| Field | Type | Required | Description |
|---|---|---|---|
event_type | UnifiedEventType | Yes | deposit_confirmed | deposit_processing | withdrawal_completed | withdrawal_failed | partial_payment |
psp_payment_id | string | Yes | PSP transaction identifier |
status | UnifiedStatus | Yes | New transaction status |
amount_credited | number | null | No | Amount to credit in USD cents (deposits) |
amount_debited | number | null | No | Amount debited in USD cents (withdrawals) |
fee_total | number | null | No | Total fees in USD cents |
blockchain_tx_id | string | null | No | On-chain transaction hash |
Possible Errors
INVALID_SIGNATUREUNKNOWN_EVENT_TYPEMALFORMED_PAYLOADAdapter MUST verify the PSP signature before processing. Throw INVALID_SIGNATURE if it fails -- the gateway returns 400 to PSP.
Adapter must return 200 quickly. Heavy processing (balance updates) happens in the orchestrator after this method returns.
getSupportedMethods(geo: string, currency: string)Promise<PaymentMethod[]>Returns the list of payment methods this PSP supports for the given country and currency.
Input Parameters
| Field | Type | Required | Description |
|---|---|---|---|
geo | string | Yes | ISO 3166-1 alpha-2 country code (e.g. "UA", "DE") |
currency | string | Yes | ISO 4217 currency code (e.g. "USD") |
Output Fields
| Field | Type | Required | Description |
|---|---|---|---|
slug | string | Yes | Method identifier used in initiate* calls (e.g. "btc", "usdt_trc20") |
name | string | Yes | Human-readable name for UI display |
min_amount | number | Yes | Minimum deposit/withdrawal amount in USD cents |
max_amount | number | Yes | Maximum deposit/withdrawal amount in USD cents |
fee_pct | number | No | Estimated fee percentage (0--100) |
logo_url | string | No | URL to method logo image |
Possible Errors
PSP_UNAVAILABLEResult may be cached per geo+currency for up to 5 minutes to reduce PSP API calls.
Frontend HTTP API
These are the HTTP endpoints the frontend calls directly. All are protected by JWT via the Auth & Brand middleware. All errors are returned as { error: { code: string, message: string }, request_id: string }.
/api/payments/depositInitiates a deposit. Returns the action the frontend must perform (redirect or show address).
Request Body
| Field | Type | Req. | Description |
|---|---|---|---|
amount | number | Yes | Amount in USD cents |
currency | string | Yes | ISO 4217 (e.g. "USD") |
method | string | Yes | Payment method slug from getSupportedMethods |
return_url | string | No | URL to return player to after payment (for redirect methods) |
Response (200)
| Field | Type | Description |
|---|---|---|
payment_id | string | Internal transaction ID -- use for status polling |
status | UnifiedStatus | Always "INITIATED" |
action | string | "redirect" | "show_address" | "show_qr" |
redirect_url | string|null | Redirect the player here when action = "redirect" |
address | string|null | Show this crypto address when action = "show_address" or "show_qr" |
tag | string|null | Show destination tag alongside address (XRP, TON). MUST be shown. |
expires_at | string|null | ISO 8601 expiry -- show countdown timer in UI |
Error Codes
400 AMOUNT_BELOW_MIN400 AMOUNT_ABOVE_MAX400 INVALID_METHOD503 PSP_UNAVAILABLE/api/payments/withdrawSubmits a withdrawal request. Frontend must collect and validate wallet_address before calling this.
Request Body
| Field | Type | Req. | Description |
|---|---|---|---|
amount | number | Yes | Amount in USD cents |
currency | string | Yes | ISO 4217 |
method | string | Yes | Payment method slug |
wallet_address | string | Yes | Player's destination wallet address |
tag | string | No | Destination tag (XRP, TON) |
Response (200)
| Field | Type | Description |
|---|---|---|
payment_id | string | Internal transaction ID |
status | UnifiedStatus | "INITIATED" or "PROCESSING" |
Error Codes
400 AMOUNT_BELOW_MIN400 AMOUNT_ABOVE_MAX400 INVALID_WALLET_ADDRESS503 PSP_UNAVAILABLE503 INSUFFICIENT_PSP_BALANCE/api/payments/:id/statusReturns the current status of a transaction. Used for frontend polling until a terminal status is reached.
Response (200)
| Field | Type | Description |
|---|---|---|
payment_id | string | Transaction ID |
status | UnifiedStatus | Current status |
amount | number|null | Credited/debited amount in USD cents |
method | string | Payment method slug |
created_at | string | ISO 8601 creation timestamp |
updated_at | string | ISO 8601 last update timestamp |
Error Codes
404 TRANSACTION_NOT_FOUND403 FORBIDDEN (transaction belongs to another user)/api/payments/methodsReturns available payment methods for the current player's geo and currency. Frontend calls this to build the method selector UI.
Response (200)
| Field | Type | Description |
|---|---|---|
methods[] | PaymentMethod[] | Array of available methods |
.slug | string | Use as "method" field in deposit/withdraw calls |
.name | string | Display name |
.min_amount | number | Minimum in USD cents |
.max_amount | number | Maximum in USD cents |
.logo_url | string|null | Icon URL |
Error Codes
503 PSP_UNAVAILABLEFrontend Status Handling
Stop polling on any terminal status. Continue polling every 5--10 seconds for non-terminal statuses. Recommended polling timeout: 15 minutes.
| Status | Label | UI Action | Terminal |
|---|---|---|---|
| INITIATED | Initiated | Show address / redirect | No |
| PROCESSING | Processing | Show spinner, keep polling | No |
| PENDING_CONFIRMATION | Awaiting confirmation | Show "waiting for blockchain confirmation" | No |
| PENDING_PARTIAL | Partial payment | Show received amount, explain shortfall | No |
| COMPLETED | Completed | Show success, stop polling | Yes |
| FAILED | Failed | Show error, offer retry | Yes |
| TIMED_OUT | Timed out | Show "payment window expired", stop polling | Yes |
| CANCELLED | Cancelled | Show cancelled state, stop polling | Yes |
Adapter Implementation Rules
Never expose PSP-native errors
Catch all PSP errors, log the raw error internally, and throw UnifiedPaymentError with the appropriate UnifiedErrorCode. The orchestrator must never see PSP-specific error objects.
Verify webhook signature first, always
handleWebhook must verify the PSP signature before parsing any payload fields. Throw INVALID_SIGNATURE immediately if it fails. For PassimPay: HMAC-SHA256(platform_id + ";" + body + ";" + secret, secret) vs x-signature header.
Map all statuses to UnifiedStatus
Every PSP-specific status code or field value must be mapped to one of the eight UnifiedStatus values. Unknown statuses must be logged and mapped to PROCESSING (never silently dropped).
Normalize all amounts to USD cents
PSPs return amounts in crypto. Adapters must convert to USD cents before populating UnifiedEvent or UnifiedResponse fields. Use the exchange rate at the time of conversion and store it for audit.
Be idempotent on initiateDeposit and initiateWithdrawal
If the same payment_id arrives twice, return the existing transaction data without creating a new PSP transaction. Store psp_payment_id keyed by payment_id to enable this check.
Respond 200 to webhooks immediately
handleWebhook must return quickly. After signature verification and payload parsing, enqueue the event for the orchestrator and return 200. Do not perform balance updates, DB writes, or external calls inside this method.
Set timeouts on all outbound PSP calls
All HTTP calls to PSP APIs must have an explicit timeout (recommended: 10s for initiate*, 5s for status). On timeout, throw PSP_UNAVAILABLE -- the orchestrator will handle cascade/retry logic.