fintrapayFintraPay APIv1.0

API Reference#

The FintraPay REST API lets you accept stablecoin payments, send payouts, manage wallets, and earn yield. All endpoints are prefixed with /v1 and return JSON responses.

Base URL
https://fintrapay.io/v1
Protocol
HTTPS only (TLS 1.2+)
Format
JSON request/response
Auth
HMAC-SHA256 or JWT Bearer
Rate Limit
100 req/min per API key
Content-Type
application/json
Quick examplecURL
curl -X POST https://fintrapay.io/v1/invoices \
  -H "Content-Type: application/json" \
  -H "X-API-Key: xfp_key_abc123def456" \
  -H "X-Timestamp: $(date +%s)" \
  -H "X-Signature: <hmac-sha256-signature>" \
  -d '{
    "amount": "100.00",
    "currency": "USDT",
    "blockchain": "tron",
    "mode": "custodial"
  }'

For AI Agents & LLMs#

Building an integration with an AI coding assistant, or pointing an agent at this API? Give it one of these instead of this page — they are plain text and always current.

Prompt an assistant with: Read https://fintrapay.io/llms-full.txt and write the code to create an invoice. The file states the signing scheme, the fee model and every parameter, so the assistant does not have to guess at any of them.

Authentication#

Two authentication methods are supported. Use HMAC-SHA256 for server-to-server API calls and JWT Bearer tokens for dashboard sessions.

HMAC-SHA256 Signing (Server-to-Server)

Every API request must include three headers. Your api_secret is never sent over the wire — it is used only to compute the HMAC signature on your server.

ParameterTypeRequiredDescription
X-API-KeystringYesYour public API key (xfp_key_...)
X-TimestampintegerYesUnix timestamp (seconds). Must be within ±30s of server time.
X-SignaturestringYesHMAC-SHA256 hex digest of the signature payload

Signature payload format: timestamp + "\n" + method + "\n" + path + "\n" + body. For requests with a query string (e.g. GET /v1/invoices?status=paid), include the query string in path exactly as sent — for example /v1/invoices?status=paid. For requests with no body, use the empty string for body.

Signature computation — your stack
# Compute signature on your server (any language), then call:
curl -X POST https://fintrapay.io/v1/invoices \
  -H "X-API-Key: xfp_key_abc123..." \
  -H "X-Timestamp: $(date +%s)" \
  -H "X-Signature: $SIGNATURE" \
  -H "Content-Type: application/json" \
  -d '{"amount":"100.00","currency":"USDT","blockchain":"tron"}'
Replay protection: The server rejects any request where the X-Timestamp differs from server time by more than 30 seconds. Always use the current Unix timestamp.

JWT Bearer (Dashboard)

For dashboard sessions, authenticate with email/password to get JWT tokens. Access tokens expire in 15 minutes; refresh tokens last 7 days.

POST/v1/merchants/loginPublic

Authenticate with email and password. Returns JWT access token (15min) and refresh token (7 days).

Request BodyJSON
{
  "email": "merchant@example.com",
  "password": "your_password"
}
ResponseJSON
{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "refresh_token": "eyJhbGciOiJIUzI1NiIs...",
  "expires_in": 900,
  "merchant": {
    "id": "uuid",
    "company_name": "Acme Corp",
    "email": "merchant@example.com"
  }
}
POST/v1/merchants/refreshPublic

Exchange a valid refresh token for a new access/refresh token pair.

Request BodyJSON
{ "refresh_token": "eyJhbGciOiJIUzI1NiIs..." }
ResponseJSON
{
  "access_token": "eyJhbG...",
  "refresh_token": "eyJhbG...",
  "expires_in": 900
}

Errors & Status Codes#

All errors return a JSON object with an error message and error code. HTTP status codes follow REST conventions.

ParameterTypeRequiredDescription
200OKNoRequest succeeded
201CreatedNoResource created successfully
400Bad RequestNoInvalid parameters or validation failure
401UnauthorizedNoInvalid or missing authentication
403ForbiddenNoInsufficient permissions
404Not FoundNoResource does not exist
409ConflictNoDuplicate resource or state conflict
429Too Many RequestsNoRate limit exceeded
500Internal ErrorNoUnexpected server error
Error response formatJSON
{
  "error": "insufficient balance for withdrawal",
  "code": "INSUFFICIENT_BALANCE"
}

Rate Limits#

Rate limits protect the API from abuse and ensure fair usage for all merchants.

ParameterTypeRequiredDescription
API endpoints100/minNoPer API key. Returns 429 when exceeded.
Auth endpoints30/minNoPer IP address. Login, register, refresh.
Account lockout5 failuresNo15-minute cooldown after 5 failed auth attempts.
Rate limit headers are included in every response: X-RateLimit-Remaining, X-RateLimit-Reset.
ParameterTypeRequiredDescription
pageintegerNoPage number (default: 1)
page_sizeintegerNoItems per page (default: 20, max: 100)
statusstringNoFilter by status (endpoint-specific)
blockchainstringNoFilter by blockchain
currencystringNoFilter by token
Paginated responseJSON
{
  "invoices": [ ... ],
  "total": 142,
  "page": 1,
  "page_size": 20
}

Invoices#

Create stablecoin payment invoices. Each invoice gets a unique payment address and branded checkout page. Supports 6 stablecoins across 7 blockchains.

POST/v1/invoicesHMAC / JWT

Create a new stablecoin invoice. Returns a checkout URL. If currency/blockchain are omitted, the customer selects their preferred token and chain on the hosted checkout page.

Parameters
amountstringrequiredPayment amount (NUMERIC string, e.g. "100.00")
currencystringToken: USDT, USDC, DAI, FDUSD, TUSD, PYUSD. Omit to let customer choose.
blockchainstringChain: tron, bsc, ethereum, solana, base, arbitrum, polygon. Omit to let customer choose.
modestring"custodial" (default) or "non_custodial"
accepted_tokensstring[]Limit which tokens customer can pick (e.g. ["USDT","USDC"]). Omit for all.
accepted_chainsstring[]Limit which chains customer can pick (e.g. ["tron","bsc"]). Omit for all.
external_idstringYour internal order/reference ID
expiry_minutesintegerExpiration in minutes (default: 60, max: 1440)
success_urlstringRedirect URL after payment confirmed
cancel_urlstringRedirect URL if payment expires or is cancelled
ResponseJSON
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "awaiting_selection",
  "amount": "100.00000000",
  "currency": "",
  "blockchain": "",
  "mode": "custodial",
  "payment_address": "",
  "checkout_url": "https://fintrapay.io/pay/a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "accepted_tokens": ["USDT","USDC","DAI","FDUSD","TUSD","PYUSD"],
  "accepted_chains": ["tron","bsc","ethereum","solana","base","arbitrum","polygon"],
  "fee_amount": "0.40000000",
  "expires_at": "2026-03-30T13: 00: 00Z",
  "created_at": "2026-03-30T12: 00: 00Z"
}
Fee: 0.4%, stamped onto the invoice at creation. In custodial mode it is deducted from the received amount at settlement. In non-custodial mode the payment goes to an address derived from your own xpub (TRON, BSC and Ethereum only).
GET/v1/invoices/{id}HMAC / JWT

Retrieve a specific invoice by ID. Returns full details including payment status, TX hash, confirmations, and fee breakdown.

ResponseJSON
{
  "id": "inv_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "confirmed_overpaid",
  "amount": "100.00000000",
  "received_amount": "110.00000000",
  "currency": "USDT",
  "blockchain": "tron",
  "payment_address": "TXyz123abc456def789...",
  "payment_tx_hash": "abc123def456...",
  "confirmations": 20,
  "fee_rate": "0.00400",
  "fee_amount": "0.40000000",
  "refunded_amount": "0.00000000",
  "overpayment_status": "pending_decision",
  "overpayment_amount": "10.00000000",
  "sender_address": "TCustomerSenderAddress...",
  "paid_at": "2026-03-29T12: 05: 32Z"
}
fee_rate is the rate stamped onto the invoice at creation — it never changes even if your account's platform_fee_rate is later updated by an admin, so it always matches the fee_amount actually charged. refunded_amount is the running total of pending/processed/completed refunds against this invoice; it caps how much can still be refunded. overpayment_status, overpayment_amount and sender_address are only present once the invoice has been overpaid (see Refunds → overpayment/accept and overpayment/refund).
GET/v1/invoicesHMAC / JWT

List all invoices with optional filters: status, currency, blockchain, date range. Paginated response.

Parameters
statusstringawaiting_selection, pending, underpaid, paid, confirmed, confirmed_overpaid, confirmed_underpaid, expired, cancelled, voided — or a comma-separated list, e.g. paid,confirmed,confirmed_overpaid,confirmed_underpaid
blockchainstringFilter by blockchain. A multi-token invoice the customer has not chosen for yet matches any chain in its accepted_chains
currencystringFilter by token. A multi-token invoice the customer has not chosen for yet matches any token in its accepted_tokens
modestringcustodial or non_custodial
pageintegerPage number
page_sizeintegerItems per page (max 100)
POST/v1/invoices/{id}/overpayment/acceptHMAC / JWT

Keep an overpayment. The surplus stays on your balance and the invoice's overpayment_status becomes accepted. No request body — the invoice ID identifies the decision.

ResponseJSON
{
  "invoice_id": "uuid",
  "overpayment_status": "accepted",
  "overpaid_amount": "12.50000000",
  "fee_amount": "0.05000000",
  "credited_amount": "12.45000000"
}
Only valid while overpayment_status is pending_decision. The default per API key is set by the overpayment policy — credit (keep) or refund.
POST/v1/invoices/{id}/overpayment/refundHMAC / JWT

Send an overpayment back to the customer. Your balance is debited immediately and the surplus is reserved while the refund is in flight, so it cannot be withdrawn from underneath the refund. No request body.

ResponseJSON
{
  "invoice_id": "uuid",
  "overpayment_status": "refund_pending",
  "overpaid_amount": "12.50000000",
  "gas_estimate": "0.27600000",
  "refund_amount": "12.22400000",
  "to_address": "TCustomerAddress..."
}
The customer receives the overage minus the network fee, quoted up front so the amount is deterministic regardless of network conditions at broadcast. An overage too small to cover the network fee is rejected — accept it instead. EVM refunds broadcast automatically; TRON and Solana are sent by our team from the admin queue.

Payouts#

Send stablecoins to any wallet address. Single payouts or batch payouts with up to 500 recipients in one call. Approval mode configurable per merchant.

POST/v1/payoutsHMAC / JWT

Create a single payout to any external wallet address.

Parameters
to_addressstringrequiredRecipient wallet address
amountstringrequiredAmount to send
currencystringrequiredToken (USDT, USDC, etc.)
blockchainstringrequiredChain (tron, bsc, etc.)
reasonstringpayment, refund, reward, airdrop, salary, other
referencestringYour internal reference
fee_deductionstring"from_amount" (default) — recipient gets amount minus fees. "from_balance" — recipient gets exactly amount; fees are debited on top.
ResponseJSON
{
  "id": "pay_b2c3d4e5-f6a7-8901-bcde-f23456789012",
  "status": "pending",
  "amount": "50.00000000",
  "currency": "USDT",
  "blockchain": "bsc",
  "to_address": "0xabc123def456...",
  "reason": "payment",
  "created_at": "2026-03-29T14: 00: 00Z"
}
If payout_auto_approve is ON, status goes directly to 'approved' and the worker broadcasts. Otherwise, status stays 'pending' until admin approves.
POST/v1/payouts/batchHMAC / JWT

Create a batch payout with multiple recipients. All must be on the same blockchain and currency.

Request BodyJSON
{
  "currency": "USDT",
  "blockchain": "bsc",
  "fee_deduction": "from_amount",
  "recipients": [
    { "to_address": "0xabc...", "amount": "50.00", "reference": "salary-001" },
    { "to_address": "0xdef...", "amount": "75.00", "reference": "salary-002" },
    { "to_address": "0x123...", "amount": "100.00", "reference": "reward-001" }
  ]
}
ResponseJSON
{
  "batch": {
    "id": "c3d4e5f6-a7b8-9012-cdef-345678901234",
    "status": "pending",
    "total_amount": "225.00000000",
    "total_count": 3,
    "currency": "USDT",
    "blockchain": "bsc",
    "created_at": "2026-03-29T14: 00: 00Z"
  },
  "payouts": [ ... ]
}
fee_deduction (optional) applies to every recipient in the batch, with the same values as POST /v1/payouts: "from_amount" (default) — each recipient gets their amount minus the per-recipient fee (payout fee + network fee), and a line that does not exceed that fee is rejected; "from_balance" — each recipient gets exactly their amount, and the per-recipient fee is debited from your balance on top. Any other value returns 422 with a fee_deduction field error. batch.total_amount is the total debited from your balance (the sum of every payout's gross_amount): the sum of the amounts under from_amount, the amounts plus every recipient's fee under from_balance. Your balance must cover that total or nothing is created. Each recipient's amount supports at most 8 decimal places (payouts are NUMERIC(20,8)) — a 9th decimal digit is rejected. A row-level validation failure (bad address, amount below the per-recipient fee, too many decimals, etc.) returns 422 with one field error per bad recipient, keyed by its position in the array — e.g. { "recipients[0].amount": "must be a positive number", "recipients[2].to_address": "invalid bsc address" } — so you can find and fix the exact row without diffing the whole payload.
GET/v1/payouts/{id}HMAC / JWT

Retrieve a specific payout. Returns status, TX hash, gas fee, and timestamps.

GET/v1/payoutsHMAC / JWT

List all payouts with optional filters: status, blockchain, currency. Paginated.

GET/v1/payouts/batchesHMAC / JWT

List all batch payouts. Paginated.

GET/v1/payouts/batches/{id}HMAC / JWT

Get batch payout details with all individual payout items.

Withdrawals#

Withdraw custodial balance to your registered wallet address. The destination address is auto-filled from your merchant profile — you cannot specify a custom to_address.

POST/v1/withdrawalsHMAC / JWT

Create a withdrawal. Destination is automatically set to your registered wallet for that blockchain.

Parameters
amountstringrequiredAmount to withdraw
currencystringrequiredToken (USDT, USDC, etc.)
blockchainstringrequiredChain (tron, bsc, etc.)
to_addressstringDestination. Falls back to your registered wallet for this chain if omitted.
fee_deductionstring"from_amount" (default) — recipient gets amount minus fees. "from_balance" — recipient gets exactly amount; fees are debited on top.
ResponseJSON
{
  "id": "wdr_d4e5f6a7-b8c9-0123-defa-456789012345",
  "status": "pending",
  "amount": "500.00000000",
  "currency": "USDT",
  "blockchain": "tron",
  "to_address": "TYourRegisteredWallet...",
  "created_at": "2026-03-29T15: 00: 00Z"
}
Fees: no flat withdrawal fee — you pay the network fee only, shown as est_gas_fee_usd. Use POST /v1/fees/estimate first to see the exact deduction and net amount. An amount that cannot cover the fees is rejected.
GET/v1/withdrawals/{id}HMAC / JWT

Retrieve a specific withdrawal by ID.

GET/v1/withdrawalsHMAC / JWT

List all withdrawals with optional filters. Paginated.

Refunds#

Issue full or partial refunds back to the customer's wallet. Same chain, same token as the original invoice.

POST/v1/invoices/{id}/refundsHMAC / JWT

Create a refund for a paid invoice. Specify the amount to refund (partial or full).

Parameters
amountstringrequiredAmount to refund
reasonstringReason for the refund
ResponseJSON
{
  "id": "ref_uuid",
  "invoice_id": "inv_uuid",
  "amount": "50.00000000",
  "currency": "USDT",
  "blockchain": "tron",
  "status": "pending",
  "created_at": "2026-03-29T16: 00: 00Z"
}
GET/v1/invoices/{id}/refundsHMAC / JWT

List every refund issued against one invoice, including partial refunds. Use this to reconcile how much of an invoice has been returned before issuing another.

GET/v1/refundsHMAC / JWT

List all refunds across every invoice. Paginated.

Parameters
statusstringpending, processed, completed, cancelled, failed
blockchainstringFilter by blockchain
pageintegerPage number
page_sizeintegerItems per page (max 100)
ResponseJSON
{
  "refunds": [ ... ],
  "total": 42,
  "page": 1,
  "page_size": 20,
  "total_pages": 3,
  "summary": {
    "refunded_amount": "1234.50000000",
    "pending_count": 2,
    "completed_count": 38
  }
}
summary is computed over every refund matching your status/blockchain filters, not just the current page. refunded_amount excludes cancelled and failed refunds — both return the reservation to your balance, so no money actually left.
GET/v1/refunds/{id}HMAC / JWT

Retrieve a specific refund by ID.

Earn#

Lock idle stablecoins from your custodial balance to earn daily interest. Flexible terms from 1–12 months with admin-configurable APY.

POST/v1/earn/contractsHMAC / JWT

Create an earn contract. Locks stablecoins from your custodial balance for a specified duration.

Parameters
amountstringrequiredPrincipal to lock
currencystringrequiredToken (USDT, USDC, etc.)
blockchainstringrequiredChain
duration_monthsintegerrequired1, 3, 6, or 12 months
ResponseJSON
{
  "id": "ern_uuid",
  "status": "active",
  "principal_amount": "1000.00000000",
  "currency": "USDT",
  "blockchain": "tron",
  "duration_months": 6,
  "term_days": 180,
  "apy": "7.00",
  "daily_interest_amount": "0.19178082",
  "accrued_interest": "0.00000000",
  "start_date": "2026-03-29T16: 00: 00Z",
  "maturity_date": "2026-09-25T16: 00: 00Z"
}
GET/v1/earn/contractsHMAC / JWT

List all earn contracts. Filter by status (active, matured, broken).

GET/v1/earn/contracts/{id}HMAC / JWT

Get earn contract details including accrued interest.

POST/v1/earn/contracts/{id}/withdraw-interestHMAC / JWT

Withdraw accrued interest. Minimum $10. Sent from treasury wallet to your registered wallet.

Request BodyJSON
{ "amount": "15.00" }
POST/v1/earn/contracts/{id}/breakHMAC / JWT

Break contract early. Principal returned minus already withdrawn interest. Undrawn interest forfeited.

Early break penalty: forfeiture of all undrawn accrued interest. Principal is always returned.
GET/v1/earn/contracts/{id}/interest-historyHMAC / JWT

Daily interest accrual history for a specific contract.

GET/v1/earn/withdrawalsHMAC / JWT

List every interest withdrawal you've requested, across all of your earn contracts, with its current status.

ResponseJSON
{
  "withdrawals": [
    {
      "id": "uuid",
      "contract_id": "ern_uuid",
      "amount": "15.00000000",
      "type": "interest",
      "status": "requested",
      "currency": "USDT",
      "blockchain": "tron",
      "created_at": "2026-03-29T16: 00: 00Z"
    }
  ]
}
Interest withdrawals are paid from the treasury wallet by our team, not broadcast automatically — status stays requested until it is sent. Use this endpoint to track where each request stands instead of assuming it completed.
GET/v1/earn/ratesPublic

Current APY by term. Public — no authentication required, so you can render live rates on a marketing page before a merchant signs in.

ResponseJSON
{
  "rates": [
    { "duration_months": 1,  "apy": 4.00,  "label": "1 Month" },
    { "duration_months": 3,  "apy": 6.00,  "label": "3 Months" },
    { "duration_months": 6,  "apy": 8.00,  "label": "6 Months" },
    { "duration_months": 12, "apy": 12.00, "label": "12 Months" }
  ]
}
Rates shown are the current configuration and apply at the moment a contract is created; an existing contract keeps the APY it was opened at.

Merchant Profile#

Manage your merchant profile, company settings, wallet addresses, and security configuration.

GET/v1/merchants/meJWT

Retrieve your full merchant profile: company info, enabled modes, KYC status, registered wallet addresses, and settings.

ResponseJSON
{
  "id": "uuid",
  "company_name": "Acme Corp",
  "email": "merchant@example.com",
  "status": "active",
  "kyc_status": "approved",
  "custodial_enabled": true,
  "non_custodial_enabled": true,
  "payout_auto_approve": false,
  "webhook_url": "https://example.com/webhooks",
  "wallet_tron": "TXyz...",
  "wallet_bsc": "0xabc...",
  "wallet_ethereum": "0xdef...",
  "wallet_solana": "...",
  "wallet_base": "0x...",
  "wallet_arbitrum": "0x...",
  "wallet_polygon": "0x..."
}
PATCH/v1/merchants/me/settingsJWT

Update merchant settings: webhook URL, wallet addresses, company name.

Request BodyJSON
{
  "webhook_url": "https://example.com/webhooks",
  "wallet_tron": "TNewWalletAddress..."
}
GET/v1/merchants/statsJWT

Get merchant dashboard stats: total invoices, volume, active contracts, etc.

Parameters
modestring"custodial" or "non_custodial" — restricts total_invoices, paid_invoices and total_volume to invoices of that mode. Omit for both modes combined (the previous, default behavior). Payouts, withdrawals and Earn counts are always custodial-only regardless of this param, since those operations exist only in custodial mode.
GET/v1/merchants/analyticsHMAC / JWT

Aggregated payment analytics: revenue per day, totals, and recurring-revenue figures. This is the data behind the dashboard Overview.

Parameters
modestring"custodial" or "non_custodial". Omit for both.
tzstringIANA time zone name, e.g. "Asia/Kolkata". Day boundaries are computed in this zone; defaults to UTC.
ResponseJSON
{
  "daily_revenue": [
    { "date": "2026-09-19", "amount": "1842.37000000", "count": 14 },
    { "date": "2026-09-20", "amount": "960.10000000", "count": 8 }
  ],
  "today": { "amount": "960.10000000", "count": 8 },
  "timezone": "Asia/Kolkata"
}
Pass tz to get days that match your own calendar. Without it a payment just after midnight local time is reported against the previous day.
POST/v1/merchants/me/kycJWT

Submit KYC documents for review. An individual account supplies identity documents; a company account supplies five, including incorporation papers.

Request BodyJSON
{
  "documents": [
    { "label": "passport", "data": "data:application/pdf;base64,JVBERi0..." }
  ]
}
Documents are sent as base64 data URIs, which inflates each file by roughly 37%. This single endpoint accepts a larger request body than the rest of the API for that reason. Approval or rejection fires the kyc.approved / kyc.rejected webhook, so an automated integration does not have to poll.

API Keys#

Manage multiple API keys with per-key settings like display name, base currency, and checkout theme.

GET/v1/merchants/me/api-keysJWT

List all your API keys with settings.

POST/v1/merchants/me/api-keysJWT

Create a new API key. Returns the secret ONCE — it cannot be retrieved later.

Request BodyJSON
{
  "display_name": "Production",
  "base_currency": "USD",
  "checkout_theme": "dark"
}
ResponseJSON
{
  "api_key": "xfp_key_...",
  "api_secret": "xfp_secret_...",
  "display_name": "Production",
  "warning": "Save your API secret now. It cannot be retrieved later."
}
The api_secret is shown ONCE. Store it securely. If lost, delete the key and create a new one.
PATCH/v1/merchants/me/api-keys/{id}JWT

Update API key settings (display_name, base_currency, checkout_theme).

DELETE/v1/merchants/me/api-keys/{id}JWT

Revoke an API key. Takes effect immediately. All requests using this key will be rejected.

POST/v1/merchants/me/regenerate-keysJWT

Rotate your primary API key and secret. The previous pair stops working the moment this returns, so deploy the new credentials before calling it.

ResponseJSON
{
  "api_key": "xfp_key_...",
  "api_secret": "xfp_secret_...",
  "created_at": "2026-09-20T10: 00: 00Z",
  "warning": "Save your API secret now. It cannot be retrieved later."
}
The secret is shown ONCE and is stored only as a hash — it cannot be read back afterwards. Rotating invalidates the old pair immediately; there is no grace period.

Balances#

Query your custodial balance per chain and token.

GET/v1/balanceHMAC / JWT

Get your custodial balance across all chains and tokens.

ResponseJSON
{
  "balances": [
    { "chain": "tron", "token": "USDT", "balance": "1250.50000000" },
    { "chain": "bsc", "token": "USDT", "balance": "340.00000000" },
    { "chain": "ethereum", "token": "USDC", "balance": "2100.75000000" },
    { "chain": "solana", "token": "USDT", "balance": "500.00000000" }
  ]
}
GET/v1/merchants/me/balance-adjustmentsHMAC / JWT

Every manual credit or debit an administrator has applied to your balance, newest first. Use it to reconcile a balance that moved without a matching invoice, payout or withdrawal.

Parameters
pageintegerPage number (default: 1)
page_sizeintegerItems per page (default: 20, max: 100)
ResponseJSON
{
  "adjustments": [
    {
      "id": "b1c2d3e4-...",
      "chain": "bsc",
      "token": "USDT",
      "amount": "25.00000000",
      "direction": "credit",
      "reason": "Goodwill credit for delayed settlement",
      "created_at": "2026-09-14T09: 12: 00Z"
    }
  ],
  "total": 1, "page": 1, "page_size": 20
}
GET/v1/merchants/me/funding-walletsHMAC / JWT

Your permanent deposit address per chain. These are the addresses to send funds to in order to top up your custodial balance directly, without creating an invoice.

ResponseJSON
{
  "wallets": [
    { "blockchain": "bsc", "address": "0xbd2242..." },
    { "blockchain": "tron", "address": "TXyzAbc..." }
  ]
}
Only chains whose deposits are actually detected are returned. An address is permanent — reuse it for every deposit on that chain. Sending a token on a chain that does not issue it loses the funds.
GET/v1/merchants/me/deposit-walletsHMAC / JWT

The per-invoice deposit addresses derived for you, with the invoice each one belongs to. Useful for reconciling an on-chain transfer back to the invoice that generated the address.

Parameters
pageintegerPage number (default: 1)
page_sizeintegerItems per page (default: 20, max: 100)

Wallets#

Manage whitelisted withdrawal wallet addresses.

GET/v1/merchants/me/walletsJWT

List all whitelisted withdrawal wallets.

POST/v1/merchants/me/walletsJWT

Add a new whitelisted wallet address.

Request BodyJSON
{ "blockchain": "tron", "address": "TNewAddress..." }
DELETE/v1/merchants/me/wallets/{id}JWT

Remove a whitelisted wallet.

Support Tickets#

Create and manage support tickets.

POST/v1/ticketsHMAC / JWT

Create a support ticket.

Request BodyJSON
{ "subject": "Payout stuck", "category": "technical", "message": "..." }
GET/v1/ticketsHMAC / JWT

List your support tickets. Paginated.

GET/v1/tickets/{id}HMAC / JWT

Get ticket details with messages.

POST/v1/tickets/{id}/replyHMAC / JWT

Reply to a ticket.

Request BodyJSON
{ "message": "Thank you, the issue is resolved." }
POST/v1/fees/estimateHMAC / JWT

Estimate fees for an invoice or payout before creating it.

Request BodyJSON
{ "amount": "100.00", "currency": "USDT", "blockchain": "tron", "type": "invoice" }

Webhooks#

Receive real-time HTTP POST notifications on payment events. Each delivery is signed with HMAC-SHA256, carries a unique event_id for idempotency, and a timestamp. 7 retries with exponential backoff (1m, 5m, 30m, 2h, 6h, 12h, 15h — ~36h total) so transient outages or maintenance windows don't drop your events. Failed deliveries can be manually resent from your dashboard or via API.

Freshness check is your responsibility. FintraPay sets X-FintraPay-Timestamp on every delivery, but does not reject anything by age — your verifier must compare |now − X-FintraPay-Timestamp| against your tolerance (we recommend ±300 seconds) to defeat replay attacks. On manual resends triggered from the dashboard or via POST /v1/merchants/me/webhook-deliveries/{id}/resend, we re-sign with a fresh timestamp so your freshness check still passes.
Webhook delivery format (v2)HTTP
POST https://your-domain.com/webhooks/fintrapay
Content-Type: application/json
User-Agent: FintraPay-Webhook/2.0
X-FintraPay-Event:      invoice.paid
X-FintraPay-Event-Id:   3f8b2a4d-1234-4abc-9def-0123456789ab
X-FintraPay-Timestamp:  2026-04-23T10:00:00Z
X-FintraPay-Signature:  <hex(HMAC-SHA256(timestamp + "\n" + body, webhook_secret))>

{
  "id":        "3f8b2a4d-1234-4abc-9def-0123456789ab",
  "event":     "invoice.paid",
  "timestamp": "2026-04-23T10:00:00Z",
  "data": {
    "invoice_id":  "inv_uuid",
    "external_id": "your-order-ref-123",
    "status":      "paid",
    "amount":      "100.00000000",
    "currency":    "USDT",
    "blockchain":  "tron",
    "tx_hash":     "abc123...",
    "paid_at":     "2026-04-23T09:59:58Z"
  }
}

Every invoice-tied event includes external_id in data — the value you passed at invoice creation, or null if you didn't supply one. Key your handler off this rather than our internal invoice_id so you can correlate webhooks back to your own orders.

Event Types

invoice.paidPayment received, status flipped to paid
invoice.confirmedPayment matured past chain confirmation threshold
invoice.expiredInvoice expired without payment
payment.receivedLegacy alias for invoice.paid (kept for compat)
payout.approvedPayout approved (auto or by admin)
payout.broadcastPayout TX broadcast to chain
payout.confirmedPayout confirmed on-chain
payout.failedPayout failed (broadcast error or admin reject)
withdrawal.approvedWithdrawal approved
withdrawal.broadcastWithdrawal TX broadcast to chain
withdrawal.confirmedWithdrawal confirmed on-chain
withdrawal.failedWithdrawal failed (broadcast error or admin reject)
earn.interest_accruedDaily interest credited to a contract
earn.maturedEarn contract reached maturity
subscription.createdA customer subscribed to one of your plans
subscription.cancelledA subscription was cancelled
deposit.detectedA deposit was detected on a deposit address
deposit.confirmedA deposit was confirmed on-chain
kyc.approvedIdentity verification approved — the account is active and shows the verified badge on checkout
kyc.rejectedIdentity verification not approved — payload carries the reason; documents can be resubmitted
merchant.security_alertSensitive account change (admin password/2FA reset, etc.) — wire this to your alerting
Verify webhook signature — your stack
import hmac, hashlib, time
from datetime import datetime

def verify_webhook(timestamp, body_bytes, signature, webhook_secret, max_age=300):
    # 1. Reject stale or future-dated deliveries to defeat replay attacks.
    ts = int(datetime.fromisoformat(timestamp.replace("Z", "+00:00")).timestamp())
    if abs(time.time() - ts) > max_age:
        return False
    # 2. Recompute signature: HMAC-SHA256 over (timestamp + "\n" + body).
    msg = timestamp.encode() + b"\n" + body_bytes
    expected = hmac.new(webhook_secret.encode(), msg, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

# Use X-FintraPay-Event-Id for idempotency: store the ID, skip duplicates.

Subscribing to specific events

By default, every event in the catalog above is delivered to your webhook_url. To opt out of specific events (e.g. silence earn.interest_accrued if you process accruals via the dashboard), use the subscriptions API:

GET/v1/merchants/me/webhook-subscriptionsJWT

List your per-event toggles. Events not listed are enabled by default.

PUT/v1/merchants/me/webhook-subscriptionsJWT

Upsert one (event, enabled) pair.

Request BodyJSON
{ "event": "earn.interest_accrued", "enabled": false }

Delivery log + manual resend

Every webhook delivery is logged with its full request payload, response status, attempt count, and final state. Use these endpoints to build a reconciliation safety net or trigger a redelivery if your endpoint missed the original (idempotent on event_id — your dedupe will recognize it as the same event).

GET/v1/merchants/me/webhook-deliveriesJWT

List your webhook deliveries. Filters: status, event, invoice_id, created_after, created_before. Paginated (page, page_size up to 100).

GET/v1/merchants/me/webhook-deliveries/{id}JWT

Get full delivery details including request payload and last response body (truncated to 1024 bytes).

POST/v1/merchants/me/webhook-deliveries/{id}/resendJWT

Re-deliver the same envelope (same event_id) with a fresh signed timestamp. Eligible: failed or delivered. Limits: 10 resends/min per merchant, 30s cooldown per delivery, 20 lifetime resends per delivery.

Reconciliation pattern

Pair the delivery log with the time-windowed invoice list to catch any drift. Pull invoices that flipped to paid or confirmed in the last 30 minutes; if your DB shows a missing webhook, fetch the corresponding delivery and resend.

Example: poll for paid invoices in the last 30 minutesHTTP
GET /v1/invoices?status=paid&updated_after=2026-04-26T10:00:00Z&page_size=100

SDKs#

Official SDKs handle HMAC signing, typed models, error handling, and webhook verification automatically.

Python SDKAvailable
Installbash
pip install fintrapay
UsagePython
from xfintrapay import FintraPay

client = FintraPay(
    api_key="xfp_key_...",
    api_secret="xfp_secret_..."
)

# Create invoice
invoice = client.create_invoice(
    amount="100.00", currency="USDT", blockchain="tron"
)

# Send batch payout
batch = client.create_batch_payout(
    currency="USDT", blockchain="bsc",
    recipients=[
        {"to_address": "0xabc...", "amount": "50.00"},
        {"to_address": "0xdef...", "amount": "75.00"},
    ]
)

# Verify webhook
is_valid = client.verify_webhook(
    payload=request.body,
    signature=request.headers["X-FintraPay-Signature"],
    timestamp=request.headers["X-FintraPay-Timestamp"]
)
PHP
Available
composer require fintrapay/fintrapay-php
Node.js
Available
npm install fintrapay
Go
Available
go get github.com/Fintra-Ltd/fintrapay-go
Java
Available
io.fintrapay:fintrapay-java

Supported Chains & Tokens#

All supported blockchains and stablecoins with contract addresses and confirmation requirements.

ParameterTypeRequiredDescription
TRONL1No2 tokens (USDT, TUSD) · 20 confirmations · ~1 min
BSCL1No5 tokens (USDT, USDC, DAI, FDUSD, TUSD) · 15 confirmations · ~45s
EthereumL1No6 tokens (USDT, USDC, DAI, FDUSD, TUSD, PYUSD) · 12 confirmations · ~3 min
SolanaL1No3 tokens (USDT, USDC, PYUSD) · 32 slots · ~13s
BaseL2No3 tokens (USDT, USDC, DAI) · 12 confirmations · ~2 min
ArbitrumL2No3 tokens (USDT, USDC, DAI) · 12 confirmations · ~2 min
PolygonL1No3 tokens (USDT, USDC, DAI) · 128 confirmations · ~5 min
USDC on TRON was discontinued by Circle in February 2025. Use USDT for TRON payments.
Payment links are reusable. Each time a customer visits the link, a fresh invoice is auto-created. The link itself never expires.
POST/v1/payment-links

Create a new payment link

Parameters
titlestringrequiredDisplay title (e.g. "Coffee Shop Payment")
slugstringCustom URL slug. Auto-generated from title if omitted
descriptionstringDescription shown to customer
amountstringFixed amount. Required if fixed_amount is true
fixed_amountbooleantrue = fixed price, false = customer enters amount. Default: true
min_amountstringMinimum amount for flexible links. Default: 1.00
max_amountstringMaximum amount for flexible links. Default: 10000.00
modestring'custodial' or 'non_custodial'. Default: custodial
success_urlstringRedirect after payment confirmed
cancel_urlstringRedirect if cancelled
ResponseJSON
{
  "id": "abc-123",
  "slug": "coffee-shop",
  "title": "Coffee Shop Payment",
  "fixed_amount": true,
  "amount": "5.00",
  "status": "active",
  "link_url": "https://fintrapay.io/p/coffee-shop",
  "total_paid": "0",
  "total_payments": 0
}
GET/v1/payment-links

List your payment links

Parameters
statusstringFilter: active, paused, archived
modestringcustodial or non_custodial — also scopes active_count and total_collected. Omit for all modes
pageintegerPage number
page_sizeintegerItems per page (max 100)
GET/v1/payment-links/{id}HMAC / JWT

Retrieve one payment link, including total_paid and total_payments. Those counters are maintained inside the settlement transaction that credits your balance, so they never disagree with the invoices beneath them.

PATCH/v1/payment-links/{id}

Update a payment link

Parameters
titlestringNew title
amountstringNew amount
statusstring'active', 'paused', or 'archived'
GET/v1/p/{slug}

Get payment link by slug (public, no auth). Returns only what the payment page shows — no IDs or totals — plus display_name, fee_bearer, and fee_rate when the payer pays the platform fee.

Parameters
slugstringrequiredPayment link slug from URL
POST/v1/p/{slug}/pay

Create invoice from payment link (public, no auth)

Parameters
amountstringRequired for flexible amount links
ResponseJSON
{
  "invoice_id": "901694b9-...",
  "checkout_url": "https://fintrapay.io/pay/901694b9-...",
  "link": { "slug": "...", "title": "...", "display_name": "...", "fee_bearer": "receiver" }
}

Subscriptions#

Recurring billing. Create plans, manage subscribers. Auto-invoiced on schedule.

Crypto can't auto-debit like credit cards. When a billing cycle starts, an invoice is auto-created and the customer is emailed a payment link. If they don't pay within the grace period, the subscription becomes past_due and retries.
POST/v1/subscription-plans

Create a subscription plan

Parameters
namestringrequiredPlan name (e.g. "Pro Plan")
amountstringrequiredBilling amount per interval
intervalstring'weekly', 'monthly', 'quarterly', 'yearly'. Default: monthly
trial_daysintegerFree trial days. Default: 0
setup_feestringOne-time fee added to the first billed invoice only (at signup, or when the trial ends). Default: 0
grace_period_hoursintegerHours before marking overdue. Default: 48
max_retriesintegerMax invoice retries before cancellation. Default: 3
slugstringCustom URL slug. Auto-generated if omitted
ResponseJSON
{
  "id": "abc-123",
  "slug": "pro-plan",
  "name": "Pro Plan",
  "amount": "9.99",
  "interval": "monthly",
  "trial_days": 7,
  "subscribe_url": "https://fintrapay.io/subscribe/pro-plan"
}
GET/v1/subscription-plans

List your plans

GET/v1/subscription-plans/{id}HMAC / JWT

Retrieve one subscription plan.

PATCH/v1/subscription-plans/{id}

Update plan (name, amount, status)

POST/v1/subscriptions

Create a subscription (merchant-initiated)

Parameters
plan_idstringrequiredPlan UUID
customer_emailstringrequiredCustomer email for invoice notifications
customer_namestringCustomer name
ResponseJSON
{
  "subscription": { "id": "...", "status": "active", "current_period_end": "..." },
  "invoice_id": "...",
  "checkout_url": "https://fintrapay.io/pay/..."
}
GET/v1/subscriptions

List subscriptions (filterable by status, plan_id)

GET/v1/subscriptions/{id}

Get subscription detail + invoice history

POST/v1/subscriptions/{id}/cancel

Cancel a subscription

POST/v1/subscriptions/{id}/pause

Pause billing

POST/v1/subscriptions/{id}/resume

Resume paused subscription

Public subscribe page

GET/v1/subscribe/{slug}

Get plan info (public, no auth). Returns only what the subscribe page shows — no IDs, subscriber counts or revenue — plus display_name, fee_bearer, and fee_rate when the payer pays the platform fee.

POST/v1/subscribe/{slug}

Customer subscribes (public)

Parameters
emailstringrequiredCustomer email
namestringCustomer name

Subscription lifecycle

ParameterTypeRequiredDescription
trialingstatusNoFree trial active. No invoice created yet.
activestatusNoSubscription is current. Invoices auto-created each billing cycle.
past_duestatusNoPayment overdue. Retry invoices being created.
pausedstatusNoMerchant paused billing. No invoices created.
cancelledstatusNoSubscription ended — by merchant, customer, or max retries exceeded.

Deep Integration API#

Persistent deposit addresses for your users. White-label wallet-like experience. No redirects.

Create users, generate static deposit addresses per blockchain, and receive webhooks when deposits are detected and confirmed. One address per user per chain — receives any supported stablecoin on that network.
POST/v1/deposit-api/users

Register a merchant-side end user

Parameters
external_user_idstringrequiredYour unique user ID
emailstringUser email
labelstringDisplay name
ResponseJSON
{ "id": "uuid", "external_user_id": "user_12345", "is_active": true }
GET/v1/deposit-api/users/{ext_id}

Get user with addresses and balances

POST/v1/deposit-api/users/{ext_id}/addresses

Generate deposit address for one chain

Parameters
blockchainstringrequiredtron, bsc, ethereum, solana, base, arbitrum, polygon
ResponseJSON
{
  "address": "0xabc...",
  "blockchain": "bsc",
  "network": "BEP-20",
  "supported_tokens": ["USDT", "USDC", "DAI", "FDUSD", "TUSD"],
  "is_active": true
}
POST/v1/deposit-api/users/{ext_id}/addresses/all

Generate addresses for all 7 chains at once

GET/v1/deposit-api/users/{ext_id}/deposits

List deposits for a user (filterable by token, chain, status)

GET/v1/deposit-api/users/{ext_id}/balances

Per-token per-chain balances for a user

GET/v1/deposit-api/users

List all of your end users. Paginated.

PATCH/v1/deposit-api/users/{ext_id}

Update an end user: email, label, is_active, is_blocked, block_reason. A blocked or deactivated user is refused new deposit addresses.

GET/v1/deposit-api/users/{ext_id}/addresses

List the deposit addresses already issued to one user, across every chain.

GET/v1/deposit-api/deposits

List deposits across all of your users. Filterable by token, blockchain and status.

GET/v1/deposit-api/deposits/{id}

Get single deposit detail

Deposit widget (white-label)

HTMLhtml
<script src="https://fintrapay.io/js/fintrapay-deposit.js"></script>
<div id="deposit"></div>
<script>
  FintraPay.deposit({
    target: '#deposit',
    apiKey: 'xfp_key_...',
    userId: 'user_12345',
    theme: 'dark',
    onDeposit: function(data) {
      console.log('Deposit confirmed!', data.token, data.amount);
    }
  });
</script>

Fee structure

ParameterTypeRequiredDescription
Platform fee0.4%NoDeducted from deposit amount
Gas feevariableNoActual sweep cost (2 TXs) converted to USD, deducted from deposit
Net amountcalculatedNoamount - platform_fee - gas_fee → credited to merchant balance

Internal Transfers#

Move custodial balance to another FintraPay merchant by email. Settles instantly on our ledger — no on-chain transaction, no network fee.

Transfers are confirmed with a one-time code emailed to the sender. Request the code first, then include it in the transfer call. A code is bound to that exact transfer and can only be used once.
GET/v1/transfers/lookupHMAC / JWT

Resolve a recipient email to a display name before sending, so the sender can confirm who they are paying.

Parameters
emailstringrequiredRecipient merchant email
POST/v1/transfers/otpHMAC / JWT

Email a one-time confirmation code to the authenticated sender. Required before a transfer can be created.

POST/v1/transfersHMAC / JWT

Transfer custodial balance to another merchant. Debit and credit happen in one transaction, so the two balances can never disagree.

Request BodyJSON
{
  "to_email": "recipient@example.com",
  "amount": "100.00",
  "currency": "USDT",
  "blockchain": "bsc",
  "note": "invoice 4412",
  "otp": "123456"
}
ResponseJSON
{
  "id": "uuid",
  "direction": "out",
  "to_email": "recipient@example.com",
  "amount": "100.00000000",
  "currency": "USDT",
  "blockchain": "bsc",
  "status": "completed",
  "created_at": "2026-08-22T10: 00: 00Z"
}
The recipient must already hold a FintraPay account. Amount is capped by your available balance on that chain and token; the check runs at write time, so a concurrent withdrawal cannot overdraw you.
GET/v1/transfersHMAC / JWT

List transfers in both directions. Each row carries a direction field of in or out.

Parameters
pageintegerPage number
page_sizeintegerItems per page (max 100)

Hosted Checkout#

Embed a payment modal on your website. Customers pay without leaving your site.

Hosted Checkout uses the same invoice API you already know. The only difference is how you display it — in an iframe modal instead of a full-page redirect.

Two integration options

ParameterTypeRequiredDescription
Redirect (default)stringNoCustomer clicks checkout_url → leaves your site → pays on fintrapay.io → returns via success_url
Hosted Checkout (iframe)stringNoModal opens on YOUR site → customer pays without leaving → callbacks fire via JavaScript

Step 1 — Add the script tag

HTMLhtml
<script src="https://fintrapay.io/js/fintrapay-checkout.js"></script>

Step 2 — Create an invoice (server-side, same as before)

cURLbash
curl -X POST https://fintrapay.io/v1/invoices \
  -H "X-API-Key: xfp_key_xxx" \
  -H "X-Timestamp: $(date +%s)" \
  -H "X-Signature: <computed>" \
  -H "Content-Type: application/json" \
  -d '{"amount": "25.00", "mode": "custodial"}'

# Response includes: { "id": "abc-123-...", "checkout_url": "..." }

Step 3 — Open the checkout modal (client-side)

JavaScriptjavascript
// After your backend creates the invoice and returns the invoice_id:

FintraPay.checkout({
  invoiceId: 'abc-123-...',   // Required — from your backend
  theme: 'dark',               // Optional — 'dark' or 'light'

  onSuccess: function(data) {
    // Payment confirmed!
    console.log('Paid! TX:', data.txHash);
    window.location.href = '/order-confirmed';
  },

  onCancel: function(data) {
    // Customer closed modal or clicked cancel
    // data.reason: 'closed' | 'backdrop' | 'escape' | 'cancel_button'
    console.log('Cancelled:', data.reason);
  },

  onExpired: function(data) {
    // 30-minute timer expired
    alert('Payment expired, please try again');
  }
});

// To programmatically close the modal:
FintraPay.close();

Full example — HTML page

HTMLhtml
<!DOCTYPE html>
<html>
<head>
  <title>My Store</title>
  <script src="https://fintrapay.io/js/fintrapay-checkout.js"></script>
</head>
<body>
  <h1>Order #1234 — $25.00</h1>
  <button id="pay-btn">Pay with Crypto</button>

  <script>
    document.getElementById('pay-btn').onclick = async function() {
      // 1. Your backend creates the invoice
      var res = await fetch('/api/create-payment', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ order_id: 1234, amount: 25.00 })
      });
      var data = await res.json();

      // 2. Open FintraPay checkout modal
      FintraPay.checkout({
        invoiceId: data.invoice_id,
        theme: 'dark',
        onSuccess: function() {
          window.location.href = '/thank-you?order=1234';
        },
        onCancel: function() {
          // Customer can try again
        },
        onExpired: function() {
          alert('Payment expired — click Pay again');
        }
      });
    };
  </script>
</body>
</html>

SDK options

ParameterTypeRequiredDescription
invoiceIdstringYesInvoice ID from POST /v1/invoices response
themestringNo'dark' or 'light' — overrides API key checkout_theme
onSuccessfunctionNoCalled when payment is confirmed. Receives { invoiceId, txHash }
onCancelfunctionNoCalled when customer closes/cancels. Receives { invoiceId, reason }
onExpiredfunctionNoCalled when 30-min timer expires. Receives { invoiceId }
onClosefunctionNoCalled when customer clicks "Close" after payment. Receives { invoiceId }
The checkout modal handles everything — coin selection, QR code, address copy, countdown timer, payment detection. Your frontend just reacts to callbacks.

Checkout endpoints

The modal above is a wrapper over three public endpoints. Call them directly if you are building your own checkout UI — none of them require authentication, because the invoice ID is the capability.

GET/v1/checkout/{id}Public

Public view of an invoice: amount, accepted tokens and chains, the payment address once a coin has been selected, expiry, and current status. Safe to expose in a browser — it carries no merchant-private fields.

ResponseJSON
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "mode": "custodial",
  "amount": "100.00000000",
  "currency": "USDT",
  "blockchain": "tron",
  "payment_address": "TXyz123abc456def789...",
  "status": "underpaid",
  "received_amount": "60.00000000",
  "confirmations": 0,
  "accepts_top_up": true,
  "expires_at": "2026-03-30T13: 00: 00Z"
}
accepts_top_up tells the checkout UI whether it's worth prompting the customer to send the remainder of an underpaid invoice: it is true only when your invoice key's partial-payment policy is 'wait'. Under 'reject' or 'accept' a later deposit is not credited toward this invoice, so the checkout must not suggest topping up.
POST/v1/checkout/{id}/selectPublic

Customer picks the coin and chain they want to pay with. This is what assigns the payment address and starts a 30-minute payment window — never later than the invoice's existing expiry.

Request BodyJSON
{ "currency": "USDT", "blockchain": "tron" }
An invoice created without a fixed currency sits in awaiting_selection until this is called; selecting on an invoice past its expiry is refused. The chosen chain determines the network fee and the number of confirmations required. Returns the same public invoice shape as GET /v1/checkout/{id}.
POST/v1/checkout/{id}/resetPublic

Clear the customer's coin selection so they can choose a different chain — for example after realising they hold USDT on TRON rather than BSC.

Refused once a payment has been detected against the assigned address, since the funds are already inbound on the original chain. Returns the same public invoice shape as GET /v1/checkout/{id}.

Changelog#

API version history and recent changes.

September 20, 2026v2.9 (Machine-readable reference)
  • New: a plain-text rendering of this entire reference at https://fintrapay.io/llms-full.txt, and a short overview at /llms.txt. This page is a client-side app, so a crawler or LLM that does not run JavaScript previously read nothing from it at all. Both files render from the same source as the OpenAPI spec, so they cannot fall out of step with it.
  • The OpenAPI spec is now also served at https://fintrapay.io/openapi.json. It was previously reachable only at /v1/docs.json, which robots.txt disallowed — so the one file built for machines was the one machines were told not to read.
  • The spec gained 12 endpoints that this page already documented but it did not: the four per-contract Earn operations, wallet listing and creation, the three webhook-delivery endpoints, webhook subscriptions, and the three support-ticket endpoints.
  • Documented endpoints that existed but were missing from this page: GET /v1/merchants/analytics, GET /v1/merchants/me/balance-adjustments, GET /v1/merchants/me/funding-wallets, GET /v1/merchants/me/deposit-wallets, POST /v1/merchants/me/kyc and POST /v1/merchants/me/regenerate-keys.
September 19, 2026v2.8 (Batch payout fees)
  • New fee_deduction field on POST /v1/payouts/batch, applied to every recipient. The default, from_amount, keeps existing behaviour: each recipient receives their amount minus the per-recipient fee. Set from_balance when every recipient must receive exactly the amount listed — each recipient's fee is then debited from your balance on top, and batch.total_amount (the total debited) includes those fees.
  • Corrected the documented POST /v1/payouts/batch response shape: the batch is returned under "batch" (with its id) next to the "payouts" array, not as top-level batch_id fields.
September 18, 2026v2.7 (Merchant-side audit fixes)
  • GET /v1/invoices/{id} and GET /v1/invoices now document fee_rate (the rate stamped on the invoice at creation, not your current platform_fee_rate), refunded_amount (running total of pending/processed/completed refunds, which caps a further refund), and overpayment_status / overpayment_amount / sender_address, present once an invoice has been overpaid.
  • GET /v1/refunds and its per-invoice counterpart now document the blockchain filter (accepted since it shipped, previously undocumented and silently ignored) and the summary object (refunded_amount, pending_count, completed_count — computed over every matching refund, not just the current page).
  • New endpoint: GET /v1/earn/withdrawals — every interest withdrawal you have requested, across all your earn contracts, with its status. Interest withdrawals are paid by hand from the treasury wallet, so use this to track a request instead of assuming it completed.
  • POST /v1/payouts/batch: documented the 8-decimal-place cap on each recipient's amount, and the per-recipient error shape on a 422 — one field error per bad row, keyed by array position (recipients[0].amount, recipients[2].to_address).
  • GET /v1/checkout/{id} now documents accepts_top_up: true only when your invoice key's partial-payment policy is 'wait', so the checkout UI knows whether prompting the customer to send the remainder of an underpaid invoice is meaningful.
  • New mode query param on GET /v1/merchants/stats: restricts invoice counts and volume to "custodial" or "non_custodial". Omit for both modes combined, the previous default behavior. Payout/withdrawal/earn counts are always custodial-only.
August 22, 2026v2.6 (Validation and docs)
  • PATCH /v1/payment-links/{id} now validates amount, min_amount and max_amount. Previously it validated nothing at all while POST /v1/payment-links did, so values the create call refused could be set on an existing link a moment later. A negative amount was accepted and stored, which did not create a negative invoice — it silently broke the link, so every customer hit an error at checkout while the link looked healthy in your dashboard. A non-numeric amount returned 500; it now returns 422 with a field error.
  • min_amount and max_amount are validated as a pair against the values already stored, so updating only one of them can no longer leave a link whose minimum exceeds its maximum — a state in which every amount a customer entered was rejected.
  • POST /v1/deposit-api/users rejects malformed metadata with 422 instead of 500. metadata is stored as JSON, and anything that was not valid JSON previously surfaced as a database error.
  • Documented endpoints that already existed but were missing from this page: overpayment accept and refund, per-invoice refund history, public Earn rates, single payment link and subscription plan reads, the deposit-API list and update calls, internal transfers, and the three public checkout endpoints the hosted modal is built on.
August 20, 2026v2.5 (Fee economics)
  • Withdrawal pricing changed: there is no flat withdrawal fee any more. You pay the network fee only, returned as est_gas_fee_usd. It was previously $0.05 plus the network fee.
  • Payout pricing changed: $0.01 per payout plus the network fee, down from $0.04. On a batch the $0.01 applies per recipient, not once per batch.
  • A network fee is charged on sweeps, payouts and withdrawals to cover the on-chain cost of moving your funds. POST /v1/fees/estimate returns the exact figure and the resulting net amount before you commit to a transfer.
  • New fee_deduction field on POST /v1/withdrawals and POST /v1/payouts. The default, from_amount, keeps existing behaviour: the recipient receives the amount minus fees. Set from_balance when the recipient must receive exactly the amount you specified — fees are then debited from your balance on top.
  • Invoice and payout responses carry the fee breakdown explicitly, so the amount that moves and the amount recorded always agree.
August 18, 2026v2.4 (Correctness)
  • Deposit crediting is now exactly-once and atomic. A confirmed deposit credits the user ledger, your merchant balance and the user total in a single transaction, and re-seeing an already-credited deposit moves no money and fires no webhook. deposit.confirmed previously fired again on every rescan.
  • TRON and Solana deposits are detected. Addresses were issued on all seven chains but only the EVM chains were being watched, so a user paying USDT-TRC20 to their assigned address was never credited.
  • A single transaction paying two of your users now records both. The deposit key is (tx_hash, destination) rather than tx_hash alone.
  • Over-length external_user_id, email or label on deposit-API users return 422 rather than 500.
  • Subscription billing anchors to the original billing day, so a subscription started on the 31st no longer drifts earlier each month.
July 30, 2026v2.3 (KYC decisions)
  • New webhook events: kyc.approved and kyc.rejected — fire when an admin decides on your identity verification. kyc.rejected carries a reason so you can surface it to the account owner and prompt a resubmission. Both include previous_status and an RFC3339 timestamp. Nothing signalled a KYC decision before this: no webhook, and no email either.
  • KYC decision emails now actually send. Approval and rejection notices were written but never wired to the endpoint that changes the status, so merchants only discovered a decision by logging in.
  • KYC document uploads are validated server-side: PNG, JPEG, WebP, GIF and PDF only, confirmed against the file’s magic bytes rather than the declared type. Limits are 10 documents, 5 MB each, 15 MB total. The stored file_type and file_size are now the server’s verified values, not the values you sent.
  • The KYC submission endpoint accepts bodies up to 24 MB (other endpoints stay at the standard limit). A five-document company submission previously failed at the edge with an opaque error.
April 26, 2026v2.2 (Security hardening + RBAC + JWT cookies)
  • Webhook retry: 3 attempts → 8 attempts over ~36h. New schedule [1m, 5m, 30m, 2h, 6h, 12h, 15h] — your endpoint can be down for a maintenance window without losing events.
  • Withdrawal velocity caps per merchant — default $10K/24h + 5/hour. Admin override via PATCH /v1/admin/merchants/{id}/fees with new fields withdrawal_velocity_24h_usd and withdrawal_velocity_1h_count.
  • New webhook event: merchant.security_alert — fires on admin password reset and 2FA reset. Payload includes action, actor_role, timestamp, and a hint string. Wire to PagerDuty / Slack so admin actions on your account are immediately visible.
  • Bulk approve gating: items above $5K force per-item review. Bulk approve/reject now goes through the service layer so every item gets its own audit log row + webhook fire.
  • Admin RBAC: 4 seeded roles (super_admin, finance_admin, support_admin, viewer) and 24 granular permissions. JWT now carries the perms array; 17 high-value admin routes (wallet transfers, payout/withdrawal approve, KYC, password/2FA reset) are gated per-permission. Existing admin tokens fall through to AdminOnly until next refresh.
  • Wallet transfer dual-approval: any admin transfer above $1K is flagged requires_dual_approval=TRUE and stays pending until a SECOND admin (different from initiator) approves it. New routes: GET/POST /v1/admin/wallet/transfers/pending, /{id}/approve, /{id}/reject. Self-approval blocked at service + DB. 24h auto-expiry.
  • JWT in browser sessions migrated to httpOnly cookies — xfp_session, xfp_refresh (HttpOnly+Secure+SameSite=Lax) plus xfp_csrf (readable double-submit token). New CSRFProtect middleware chained before JWTAuth. Bearer-token (SDK / server-to-server) path bypasses CSRF — no behaviour change for HMAC API callers.
  • New endpoint: POST /v1/logout — blacklists current JWT and clears the session cookies. Body optional; identity comes from cookie or Authorization header.
  • GET /v1/merchants/refresh now reads the xfp_refresh cookie if no body is supplied — refresh works without ever touching the token in JS.
  • Stdlib DB migration runner shipped — scripts/migrations/{date}_{seq}_{name}.up.sql + scripts/migrate.sh. 5 migrations tracked in schema_migrations.
April 26, 2026v2.1 (Webhook redelivery + reconciliation)
  • Merchant-facing webhook delivery log — GET /v1/merchants/me/webhook-deliveries with filters: status, event, invoice_id, created_after, created_before
  • Single-delivery detail — GET /v1/merchants/me/webhook-deliveries/{id} returns full payload + last response body
  • Manual resend — POST /v1/merchants/me/webhook-deliveries/{id}/resend re-delivers the same envelope (same event_id) with a fresh signed timestamp
  • Resend limits: 10/min per merchant, 30s cooldown per delivery, 20 lifetime resends per delivery
  • Atomic resend — single SQL UPDATE enforces ownership + status + cooldown + cap (no TOCTOU race)
  • Cross-merchant access returns 404 (not 403) to prevent ID enumeration
  • Time-range filters added to GET /v1/invoices — created_after, created_before, updated_after, updated_before (RFC3339)
  • Replay path on manual resends now uses fresh X-FintraPay-Timestamp — passes ±5 min freshness checks even when resending old deliveries
  • New "Webhooks" page in merchant dashboard with delivery log + one-click resend
April 23, 2026v2.0 (Webhooks)
  • Webhook subsystem rewrite — every declared event now actually fires (was 0 of 12 before)
  • New v2 envelope: {id, event, timestamp, data} with unique event_id UUID for idempotency
  • New headers: X-FintraPay-Event, X-FintraPay-Event-Id, X-FintraPay-Timestamp, X-FintraPay-Signature
  • Signing changed: HMAC-SHA256 over (timestamp + "\n" + body) — defeats replay attacks
  • 18 events now wired: invoice.{paid,confirmed,expired}, payout.{approved,broadcast,confirmed,failed}, withdrawal.{approved,broadcast,confirmed,failed}, earn.{interest_accrued,matured}, deposit.{detected,confirmed}, subscription.{created,cancelled}, payment.received (legacy alias)
  • Per-merchant per-event subscription toggles — GET/PUT /v1/merchants/me/webhook-subscriptions (opt-out model, default = all enabled)
  • Redis SETNX dedup on event_id (24h TTL) — duplicate publishes silently dropped
  • Persisted delivery history — every attempt writes a webhook_deliveries row with full payload
  • Durable retry — DB-backed fallback catches retries when Redis flushes
  • New invoice.expired cron — fires within 60s of expires_at
  • New invoice.confirmed cron — promotes paid → confirmed after per-chain time-based lag (CLAUDE.md confirmations × avg block time)
  • Backward compatible: payment.received still emitted alongside invoice.paid
April 4, 2026v1.6
  • Deep Integration API — persistent deposit addresses for merchant end-users
  • New endpoints: /deposit-api/users (CRUD), /addresses (generate), /deposits (list), /balances
  • Static address per user per chain — receives any supported stablecoin
  • Deposit widget (fintrapay-deposit.js) — white-label embeddable UI with network selector, QR, copy
  • Blockchain pollers enhanced to watch deposit_destinations alongside invoice addresses
  • Deposit events: detecting → confirmed → swept, with 0.4% fee + gas fee
  • Merchant dashboard: Deposit Users + Deposits pages
  • Admin panel: Deposits overview with platform stats
April 4, 2026v1.5
  • Subscriptions / Recurring Billing — plans, subscribers, auto-invoicing, trial periods, grace period, retry logic
  • New endpoints: subscription-plans (CRUD), subscriptions (CRUD + cancel/pause/resume), /subscribe/{slug} (public)
  • Billing cron: hourly — creates invoices, handles overdue, cancels after max retries, activates trials
  • Merchant dashboard: Subscriptions page with plans + subscribers tabs
  • Admin panel: Subscriptions overview with platform MRR stats
  • Public subscribe page at /subscribe/{slug}
April 4, 2026v1.4
  • Payment Links — reusable payment URLs with fixed or flexible amounts, QR codes, auto-invoice creation
  • New endpoints: POST/GET /v1/payment-links, PATCH /v1/payment-links/{id}, GET/POST /v1/p/{slug}
  • Merchant dashboard: Payment Links page with create/edit modal, copy URL, QR download, pause/archive
  • Admin panel: Payment Links overview page with platform-wide stats
  • Invoice source now includes "payment_link" alongside "api" and "manual"
  • Public payment link page at /p/{slug} — branded, themed, mobile-responsive
April 4, 2026v1.3
  • Hosted Checkout — drop-in iframe SDK (fintrapay-checkout.js) for embedded payments
  • Invoice expiry changed from 60 minutes to 30 minutes
  • Address reuse — re-selecting same coin+chain on multi-token invoice reuses cached address
  • Multi-token invoices now expire after 30 minutes (previously never expired until selection)
  • success_url and cancel_url now correctly applied from API key settings on all invoice types
  • Cancel payment button on checkout page
  • Return to merchant button after payment confirmation
March 30, 2026v1.2
  • currency and blockchain now optional — omit both to let customer choose on checkout
  • checkout_url returned in every invoice response
  • success_url and cancel_url — per API key + per invoice override for redirects
  • OpenAPI 3.0 spec at GET /v1/docs.json — machine-readable API documentation
  • POST /checkout/{id}/reset — customer can change payment method after selecting
  • Webhook secret visible in API Management page per key
  • All 5 SDKs updated (Python, Node.js, PHP, Go, Java)
March 29, 2026v1.1
  • Email OTP verification on registration
  • KYC document upload (individual: 2 docs, company: 5 docs)
  • Terms & conditions checkbox on signup
  • Welcome email sent after account creation
  • API base URL corrected to https://fintrapay.io/v1
  • 5 EVM splitter contracts deployed on mainnet
March 2026v1.0
  • Initial API release
  • 30+ endpoints across 6 resource types
  • Python SDK (pip install fintrapay)
  • HMAC-SHA256 authentication
  • 7 blockchains, 6 stablecoins
  • Per API key: display_name, base_currency, checkout_theme
  • Fiat-to-stablecoin conversion (12 currencies)
  • Real-time webhooks with HMAC verification