Unified Interface

IPaymentProvider -- a unified contract for all PSP adapters. Full spec for backend and frontend.

Phase 1
Spec
Backend
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.

1.

TypeScript Types

IPaymentProvider interface
// 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 & Response types
// ── 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'
Error types
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 fields
2.

Method 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

FieldTypeRequiredDescription
payment_idstring
Yes
Unique idempotency key, generated by orchestrator
user_idstring
Yes
Player identifier
brand_idstring
Yes
Brand / tenant identifier
amountnumber
Yes
Amount in USD cents (integer)
currencystring
Yes
ISO 4217 currency code (e.g. "USD")
methodstring
Yes
Payment method slug (e.g. "btc", "usdt_trc20")
return_urlstring
No
URL to redirect player after payment (if PSP supports)
metadataRecord<string, string>
No
Arbitrary key-value pairs passed through to webhook events

Output Fields

FieldTypeRequiredDescription
payment_idstring
Yes
Echo of the input payment_id
psp_payment_idstring
Yes
PSP-internal transaction identifier (store for idempotency)
statusUnifiedStatus
Yes
Always INITIATED at this stage
actionUnifiedAction
Yes
redirect | show_address | show_qr
redirect_urlstring | null
No
Set when action = redirect
addressstring | null
No
Crypto address, set when action = show_address
tagstring | null
No
Destination tag / memo (XRP, TON etc.)
expires_atstring | null
No
ISO 8601 expiry timestamp (for invoice methods)

Possible Errors

PSP_UNAVAILABLEINVALID_METHODAMOUNT_BELOW_MINAMOUNT_ABOVE_MAXCURRENCY_NOT_SUPPORTED

Must 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

FieldTypeRequiredDescription
payment_idstring
Yes
Unique idempotency key
user_idstring
Yes
Player identifier
brand_idstring
Yes
Brand / tenant identifier
amountnumber
Yes
Amount in USD cents (integer)
currencystring
Yes
ISO 4217 currency code
methodstring
Yes
Payment method slug
wallet_addressstring
Yes
Player's destination wallet address
tagstring
No
Destination tag / memo (required for XRP, TON)

Output Fields

FieldTypeRequiredDescription
payment_idstring
Yes
Echo of the input payment_id
psp_payment_idstring
Yes
PSP-internal transaction identifier
statusUnifiedStatus
Yes
INITIATED or PROCESSING depending on PSP

Possible Errors

PSP_UNAVAILABLEINVALID_WALLET_ADDRESSAMOUNT_BELOW_MINAMOUNT_ABOVE_MAXINSUFFICIENT_PSP_BALANCE

Adapter 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

FieldTypeRequiredDescription
psp_payment_idstring
Yes
PSP-internal transaction ID returned by initiate* methods

Output Fields

FieldTypeRequiredDescription
psp_payment_idstring
Yes
Echo of the input identifier
statusUnifiedStatus
Yes
Current unified status
amount_creditednumber | null
No
Final credited amount in USD cents
blockchain_tx_idstring | null
No
On-chain transaction hash if available

Possible Errors

PSP_UNAVAILABLETRANSACTION_NOT_FOUND

This 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

FieldTypeRequiredDescription
bodyunknown
Yes
Raw parsed JSON body of the webhook request
headersRecord<string, string>
Yes
HTTP headers (needed for signature verification)
raw_bodyBuffer
Yes
Raw unmodified request body bytes for HMAC verification

Output Fields

FieldTypeRequiredDescription
event_typeUnifiedEventType
Yes
deposit_confirmed | deposit_processing | withdrawal_completed | withdrawal_failed | partial_payment
psp_payment_idstring
Yes
PSP transaction identifier
statusUnifiedStatus
Yes
New transaction status
amount_creditednumber | null
No
Amount to credit in USD cents (deposits)
amount_debitednumber | null
No
Amount debited in USD cents (withdrawals)
fee_totalnumber | null
No
Total fees in USD cents
blockchain_tx_idstring | null
No
On-chain transaction hash

Possible Errors

INVALID_SIGNATUREUNKNOWN_EVENT_TYPEMALFORMED_PAYLOAD

Adapter 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

FieldTypeRequiredDescription
geostring
Yes
ISO 3166-1 alpha-2 country code (e.g. "UA", "DE")
currencystring
Yes
ISO 4217 currency code (e.g. "USD")

Output Fields

FieldTypeRequiredDescription
slugstring
Yes
Method identifier used in initiate* calls (e.g. "btc", "usdt_trc20")
namestring
Yes
Human-readable name for UI display
min_amountnumber
Yes
Minimum deposit/withdrawal amount in USD cents
max_amountnumber
Yes
Maximum deposit/withdrawal amount in USD cents
fee_pctnumber
No
Estimated fee percentage (0--100)
logo_urlstring
No
URL to method logo image

Possible Errors

PSP_UNAVAILABLE

Result may be cached per geo+currency for up to 5 minutes to reduce PSP API calls.

3.

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

POST
/api/payments/deposit

Initiates a deposit. Returns the action the frontend must perform (redirect or show address).

Request Body

FieldTypeReq.Description
amountnumber
Yes
Amount in USD cents
currencystring
Yes
ISO 4217 (e.g. "USD")
methodstring
Yes
Payment method slug from getSupportedMethods
return_urlstring
No
URL to return player to after payment (for redirect methods)

Response (200)

FieldTypeDescription
payment_idstringInternal transaction ID -- use for status polling
statusUnifiedStatusAlways "INITIATED"
actionstring"redirect" | "show_address" | "show_qr"
redirect_urlstring|nullRedirect the player here when action = "redirect"
addressstring|nullShow this crypto address when action = "show_address" or "show_qr"
tagstring|nullShow destination tag alongside address (XRP, TON). MUST be shown.
expires_atstring|nullISO 8601 expiry -- show countdown timer in UI

Error Codes

400 AMOUNT_BELOW_MIN400 AMOUNT_ABOVE_MAX400 INVALID_METHOD503 PSP_UNAVAILABLE
POST
/api/payments/withdraw

Submits a withdrawal request. Frontend must collect and validate wallet_address before calling this.

Request Body

FieldTypeReq.Description
amountnumber
Yes
Amount in USD cents
currencystring
Yes
ISO 4217
methodstring
Yes
Payment method slug
wallet_addressstring
Yes
Player's destination wallet address
tagstring
No
Destination tag (XRP, TON)

Response (200)

FieldTypeDescription
payment_idstringInternal transaction ID
statusUnifiedStatus"INITIATED" or "PROCESSING"

Error Codes

400 AMOUNT_BELOW_MIN400 AMOUNT_ABOVE_MAX400 INVALID_WALLET_ADDRESS503 PSP_UNAVAILABLE503 INSUFFICIENT_PSP_BALANCE
GET
/api/payments/:id/status

Returns the current status of a transaction. Used for frontend polling until a terminal status is reached.

Response (200)

FieldTypeDescription
payment_idstringTransaction ID
statusUnifiedStatusCurrent status
amountnumber|nullCredited/debited amount in USD cents
methodstringPayment method slug
created_atstringISO 8601 creation timestamp
updated_atstringISO 8601 last update timestamp

Error Codes

404 TRANSACTION_NOT_FOUND403 FORBIDDEN (transaction belongs to another user)
GET
/api/payments/methods

Returns available payment methods for the current player's geo and currency. Frontend calls this to build the method selector UI.

Response (200)

FieldTypeDescription
methods[]PaymentMethod[]Array of available methods
.slugstringUse as "method" field in deposit/withdraw calls
.namestringDisplay name
.min_amountnumberMinimum in USD cents
.max_amountnumberMaximum in USD cents
.logo_urlstring|nullIcon URL

Error Codes

503 PSP_UNAVAILABLE
4.

Frontend Status Handling

Stop polling on any terminal status. Continue polling every 5--10 seconds for non-terminal statuses. Recommended polling timeout: 15 minutes.

StatusLabelUI ActionTerminal
INITIATEDInitiatedShow address / redirect
No
PROCESSINGProcessingShow spinner, keep polling
No
PENDING_CONFIRMATIONAwaiting confirmationShow "waiting for blockchain confirmation"
No
PENDING_PARTIALPartial paymentShow received amount, explain shortfall
No
COMPLETEDCompletedShow success, stop polling
Yes
FAILEDFailedShow error, offer retry
Yes
TIMED_OUTTimed outShow "payment window expired", stop polling
Yes
CANCELLEDCancelledShow cancelled state, stop polling
Yes
5.

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.

DEPO44 | UNIFIED INTERFACE SPEC v1 | PHASE 1