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.
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.
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.
| Parameter | Type | Required | Description |
|---|---|---|---|
X-API-Key | string | Yes | Your public API key (xfp_key_...) |
X-Timestamp | integer | Yes | Unix timestamp (seconds). Must be within ±30s of server time. |
X-Signature | string | Yes | HMAC-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.
# 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"}'
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.
Errors & Status Codes#
All errors return a JSON object with an error message and error code. HTTP status codes follow REST conventions.
| Parameter | Type | Required | Description |
|---|---|---|---|
200 | OK | No | Request succeeded |
201 | Created | No | Resource created successfully |
400 | Bad Request | No | Invalid parameters or validation failure |
401 | Unauthorized | No | Invalid or missing authentication |
403 | Forbidden | No | Insufficient permissions |
404 | Not Found | No | Resource does not exist |
409 | Conflict | No | Duplicate resource or state conflict |
429 | Too Many Requests | No | Rate limit exceeded |
500 | Internal Error | No | Unexpected server error |
{
"error": "insufficient balance for withdrawal",
"code": "INSUFFICIENT_BALANCE"
}Rate Limits#
Rate limits protect the API from abuse and ensure fair usage for all merchants.
| Parameter | Type | Required | Description |
|---|---|---|---|
API endpoints | 100/min | No | Per API key. Returns 429 when exceeded. |
Auth endpoints | 30/min | No | Per IP address. Login, register, refresh. |
Account lockout | 5 failures | No | 15-minute cooldown after 5 failed auth attempts. |
X-RateLimit-Remaining, X-RateLimit-Reset.Pagination#
All list endpoints support cursor-based pagination via query parameters.
| Parameter | Type | Required | Description |
|---|---|---|---|
page | integer | No | Page number (default: 1) |
page_size | integer | No | Items per page (default: 20, max: 100) |
status | string | No | Filter by status (endpoint-specific) |
blockchain | string | No | Filter by blockchain |
currency | string | No | Filter by token |
{
"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.
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.
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.
Refunds#
Issue full or partial refunds back to the customer's wallet. Same chain, same token as the original invoice.
Earn#
Lock idle stablecoins from your custodial balance to earn daily interest. Flexible terms from 1–12 months with admin-configurable APY.
Merchant Profile#
Manage your merchant profile, company settings, wallet addresses, and security configuration.
API Keys#
Manage multiple API keys with per-key settings like display name, base currency, and checkout theme.
Balances#
Query your custodial balance per chain and token.
Wallets#
Manage whitelisted withdrawal wallet addresses.
Support Tickets#
Create and manage support tickets.
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.
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.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 paidinvoice.confirmedPayment matured past chain confirmation thresholdinvoice.expiredInvoice expired without paymentpayment.receivedLegacy alias for invoice.paid (kept for compat)payout.approvedPayout approved (auto or by admin)payout.broadcastPayout TX broadcast to chainpayout.confirmedPayout confirmed on-chainpayout.failedPayout failed (broadcast error or admin reject)withdrawal.approvedWithdrawal approvedwithdrawal.broadcastWithdrawal TX broadcast to chainwithdrawal.confirmedWithdrawal confirmed on-chainwithdrawal.failedWithdrawal failed (broadcast error or admin reject)earn.interest_accruedDaily interest credited to a contractearn.maturedEarn contract reached maturitysubscription.createdA customer subscribed to one of your planssubscription.cancelledA subscription was cancelleddeposit.detectedA deposit was detected on a deposit addressdeposit.confirmedA deposit was confirmed on-chainkyc.approvedIdentity verification approved — the account is active and shows the verified badge on checkoutkyc.rejectedIdentity verification not approved — payload carries the reason; documents can be resubmittedmerchant.security_alertSensitive account change (admin password/2FA reset, etc.) — wire this to your alertingimport 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:
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).
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.
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.
pip install fintrapay
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"] )
composer require fintrapay/fintrapay-phpnpm install fintrapaygo get github.com/Fintra-Ltd/fintrapay-goio.fintrapay:fintrapay-javaSupported Chains & Tokens#
All supported blockchains and stablecoins with contract addresses and confirmation requirements.
| Parameter | Type | Required | Description |
|---|---|---|---|
TRON | L1 | No | 2 tokens (USDT, TUSD) · 20 confirmations · ~1 min |
BSC | L1 | No | 5 tokens (USDT, USDC, DAI, FDUSD, TUSD) · 15 confirmations · ~45s |
Ethereum | L1 | No | 6 tokens (USDT, USDC, DAI, FDUSD, TUSD, PYUSD) · 12 confirmations · ~3 min |
Solana | L1 | No | 3 tokens (USDT, USDC, PYUSD) · 32 slots · ~13s |
Base | L2 | No | 3 tokens (USDT, USDC, DAI) · 12 confirmations · ~2 min |
Arbitrum | L2 | No | 3 tokens (USDT, USDC, DAI) · 12 confirmations · ~2 min |
Polygon | L1 | No | 3 tokens (USDT, USDC, DAI) · 128 confirmations · ~5 min |
Payment Links#
Reusable payment URLs. Share anywhere — QR codes, social media, email, printed receipts. Never expires.
Subscriptions#
Recurring billing. Create plans, manage subscribers. Auto-invoiced on schedule.
Public subscribe page
Subscription lifecycle
| Parameter | Type | Required | Description |
|---|---|---|---|
trialing | status | No | Free trial active. No invoice created yet. |
active | status | No | Subscription is current. Invoices auto-created each billing cycle. |
past_due | status | No | Payment overdue. Retry invoices being created. |
paused | status | No | Merchant paused billing. No invoices created. |
cancelled | status | No | Subscription ended — by merchant, customer, or max retries exceeded. |
Deep Integration API#
Persistent deposit addresses for your users. White-label wallet-like experience. No redirects.
Deposit widget (white-label)
<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
| Parameter | Type | Required | Description |
|---|---|---|---|
Platform fee | 0.4% | No | Deducted from deposit amount |
Gas fee | variable | No | Actual sweep cost (2 TXs) converted to USD, deducted from deposit |
Net amount | calculated | No | amount - 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.
Hosted Checkout#
Embed a payment modal on your website. Customers pay without leaving your site.
Two integration options
| Parameter | Type | Required | Description |
|---|---|---|---|
Redirect (default) | string | No | Customer clicks checkout_url → leaves your site → pays on fintrapay.io → returns via success_url |
Hosted Checkout (iframe) | string | No | Modal opens on YOUR site → customer pays without leaving → callbacks fire via JavaScript |
Step 1 — Add the script tag
<script src="https://fintrapay.io/js/fintrapay-checkout.js"></script>
Step 2 — Create an invoice (server-side, same as before)
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)
// 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
<!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
| Parameter | Type | Required | Description |
|---|---|---|---|
invoiceId | string | Yes | Invoice ID from POST /v1/invoices response |
theme | string | No | 'dark' or 'light' — overrides API key checkout_theme |
onSuccess | function | No | Called when payment is confirmed. Receives { invoiceId, txHash } |
onCancel | function | No | Called when customer closes/cancels. Receives { invoiceId, reason } |
onExpired | function | No | Called when 30-min timer expires. Receives { invoiceId } |
onClose | function | No | Called when customer clicks "Close" after payment. Receives { invoiceId } |
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.
Changelog#
API version history and recent changes.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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
- 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
- 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
- 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}
- 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
- 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
- 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)
- 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
- 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