REX Pay Merchant Integration Guide
Version: 1.0
Base URL: https://api.rexpay.co
Dashboard: https://app.rexpay.co
Table of Contents
- Overview
- Getting Started
- Settlement Configuration
- Payment Flow
- API Reference
- Webhook Integration
- Hosted Checkout
- Multi-Chain Considerations
- Error Handling
- Testing
1. Overview
REX Pay (Regen Exchange) is a non-custodial crypto payment processing platform. It enables merchants to accept digital assets directly into their own wallets — REX Pay never holds your funds.
Supported Chains
| Chain | Code | Symbol | Type | Confirmations (default) |
|---|---|---|---|---|
| Dogecoin | DOGE |
DOGE | UTXO | 1 |
| Litecoin | LTC |
LTC | UTXO | 3 |
| USDT on TRON | USDT_TRC20 |
USDT | Account (TRC-20 token) | 1 |
Non-Custodial Model
When you configure settlement, you provide either:
- An XPUB (extended public key) — REX Pay derives a unique receiving address per invoice from your HD wallet. You control the private keys; REX Pay only holds the public key needed to generate addresses.
- A single address — All payments go to one address. Because the same address is shared across invoices, REX Pay applies a small amount-salt to disambiguate payments (UTXO chains only).
Funds arrive in your wallet directly from the payer. REX Pay monitors the blockchain and notifies your system when payment is received and confirmed.
Stripe-Compatible API
The REX Pay API is modeled after Stripe's PaymentIntents API. If you have existing Stripe integration experience, you will find the concepts familiar: create a PaymentIntent, redirect your customer to hosted checkout or build your own UI, then handle confirmation webhooks.
2. Getting Started
2.1 Register as a Merchant
Contact the REX Pay team or sign up through the dashboard at https://app.rexpay.co. You will need:
- Business name and contact email
- Intended chains (DOGE, LTC, USDT TRC-20, or all)
- Settlement wallet details (XPUB or single address per chain)
2.2 API Keys
REX Pay issues two types of API keys per merchant:
| Type | Format | Purpose |
|---|---|---|
| Test key | rp_test_<32 chars> |
Sandbox — no real funds, testnet addresses |
| Live key | rp_live_<32 chars> |
Production — real funds, mainnet addresses |
API keys are shown once at creation and cannot be retrieved afterwards. Store them securely.
Authentication: Pass your API key in the Authorization header as a Bearer token:
2.3 Configure Settlement
Before you can create payment intents on a chain, you must configure settlement for that chain in the dashboard. Settlement configuration is per-chain. See Section 3 for details.
2.4 Choose Supported Chains
You only need to configure the chains you intend to accept. Each chain you configure becomes available as a valid value for the chain parameter when creating PaymentIntents.
3. Settlement Configuration
3.1 DOGE — Dogecoin
Recommended derivation path: m/44'/3'/0'
| Setting | Description |
|---|---|
| Settlement mode | xpub (recommended) or address |
| XPUB | Account-level extended public key from your DOGE HD wallet |
| Derivation cursor | Auto-incremented; REX Pay generates m/44'/3'/0'/0/{index} per invoice |
| Default confirmations | 1 |
| Default expiration | 30 minutes |
| Underpayment tolerance | 1% of expected amount |
Single address mode (DOGE):
If you use a single address instead of XPUB, REX Pay applies a micro-amount salt to each invoice to disambiguate simultaneous payments. Salt is added in increments of 0.000001 DOGE up to 50,000 steps. Once all slots are occupied by active invoices, new invoice creation will fail with salt_exhausted until older invoices expire or confirm.
Generating a DOGE XPUB
Using a BIP44-compatible wallet (Ledger, Trezor, Electrum):
- Navigate to the Dogecoin account (
m/44'/3'/0') - Export the account-level extended public key
- The key begins with
dgub(mainnet) ortgub(testnet) - Paste this key into the dashboard under DOGE settlement settings
3.2 LTC — Litecoin
Recommended derivation path: m/44'/2'/0'
| Setting | Description |
|---|---|
| Settlement mode | xpub (recommended) or address |
| XPUB | Account-level extended public key from your LTC HD wallet |
| Derivation cursor | Auto-incremented; derives m/44'/2'/0'/0/{index} per invoice |
| Default confirmations | 3 |
| Default expiration | 30 minutes |
| Underpayment tolerance | 1% of expected amount |
Single address mode (LTC):
Salt increments are 0.00000001 LTC (1 satoshi). Up to 50,000 concurrent active salt slots.
Address formats accepted:
| Format | Prefix | Notes |
|---|---|---|
| Legacy (P2PKH) | L, M |
Most compatible |
| Native SegWit (P2WPKH) | ltc1 |
Lower fees for payers |
| Testnet | m, n, tltc1 |
Test mode only |
3.3 USDT TRC-20
Recommended derivation path: m/44'/195'/0' (TRON coin type)
| Setting | Description |
|---|---|
| Settlement mode | xpub (recommended) or address |
| XPUB | TRON account-level extended public key |
| Derivation cursor | Auto-incremented; derives unique TRON address per invoice |
| Default confirmations | 1 |
| Default expiration | 30 minutes |
| Underpayment tolerance | 0.5% of expected amount |
No salt for USDT: TRON is an account-based chain. Each invoice receives a unique derived address in XPUB mode. Single-address mode is also supported (no salt — the address is shared and amounts must be exact, or within the tolerance threshold, for reconciliation). There is no salt mechanism for TRON.
USDT amounts are always in USD:
USDT TRC-20 is pegged 1:1 to USD. When you specify amount_doge for a USDT invoice, the value represents the USDT amount (e.g., "amount_doge": "25.00" means 25 USDT). You may also specify amount_fiat with fiat_currency: "USD" and the system will use a 1:1 rate.
TRON address format:
All TRON addresses (mainnet and Nile testnet) start with T and are 34 characters long (base58check encoded).
4. Payment Flow
4.1 Step-by-Step Flow
- Create PaymentIntent — Your backend calls
POST /v1/payment_intentswith the amount and chain. REX Pay generates a unique receiving address and returns acheckout_url. - Redirect Customer — Send your customer to the hosted checkout URL, or use the
addressandamount_dogefields to build your own payment UI. - Customer Pays — The customer sends crypto to the displayed address and exact amount.
- Detection — REX Pay's blockchain monitor detects the incoming transaction (status transitions to
detected, thenprocessing). - Confirmation — After the required number of block confirmations, the intent status transitions to
confirmed. - Webhook — REX Pay fires a
payment_intent.confirmedwebhook to your registered endpoint. - Fulfill Order — Your server receives the webhook, verifies the signature, and fulfills the order.
4.2 Sequence Diagram
Merchant Backend REX Pay API Customer Browser Blockchain
| | | |
|--POST /payment_intents--> | |
| (amount, chain, urls) | | |
|<---201 + checkout_url---| | |
| | | |
|---redirect customer-----> | |
| |<--GET /public/checkout/{secret}------------|
| |---checkout details (address, amount)------->
| | | |
| | customer sends DOGE----------->
| | | |
| |<--blockchain monitor detects tx-------------|
| | (status: detected → processing) |
| | | |
| |<--N confirmations--------------------------------|
| | (status: confirmed) | |
| | | |
|<--POST webhook (confirmed) | |
| (verify signature) | | |
| | | |
|---fulfill order | | |
| |<--GET /public/checkout/{secret}/status-----|
| |---{status: confirmed, success_url}-------->
| | | |
| | customer redirected to success_url
4.3 Payment Intent Status Values
| Status | Description |
|---|---|
requires_payment |
Waiting for the customer to send funds |
detected |
Incoming transaction seen in mempool, not yet confirmed |
processing |
Transaction confirmed but waiting for required confirmation count |
confirmed |
Fully confirmed — payment complete |
expired |
The invoice expired before payment was received |
canceled |
Canceled via API before any payment was detected |
flagged |
Anomalous payment (underpayment, overpayment, late payment) — review required |
5. API Reference
Authentication
All authenticated endpoints require an API key in the Authorization header:
Rate Limits
| Key type | Limit |
|---|---|
| Live API key | Configurable per-minute (see dashboard) |
| Test API key | Higher limit for development |
| Public checkout endpoints | IP-based, per-minute sliding window |
Rate limit headers are returned on every response:
If the limit is exceeded, you receive 429 Too Many Requests with a Retry-After header.
Idempotency
POST /v1/payment_intents supports idempotent requests. Pass a unique string in the Idempotency-Key header to safely retry requests without creating duplicate intents. The idempotency cache is scoped per merchant and expires after 24 hours.
POST /v1/payment_intents
Create a new PaymentIntent.
Authentication: Required (API key)
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
chain |
string | No | Chain code: DOGE, LTC, USDT_TRC20. Defaults to DOGE. |
amount_doge |
decimal | Conditional | Amount in the chain's native unit. Required if amount_fiat is not provided. Must be > 0. |
amount_fiat |
decimal | Conditional | Fiat amount to convert. Required if amount_doge is not provided. Must be > 0. |
fiat_currency |
string | Conditional | Required when amount_fiat is set. One of: USD, EUR, UAH, PLN, GBP. |
merchant_order_id |
string | No | Your internal order/reference ID (max 200 chars). |
metadata |
object | No | Arbitrary key-value pairs for your records. |
customer_email |
string | No | Customer email address. |
success_url |
string | No | URL to redirect the customer after confirmed payment. |
cancel_url |
string | No | URL to redirect if the customer cancels checkout. |
confirmations |
integer | No | Override the merchant default. Range: 0–100. |
expires_in_minutes |
integer | No | Override invoice lifetime. Range: 5–1440. |
Example request — fiat amount with DOGE:
POST /v1/payment_intents
Authorization: Bearer rp_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
Idempotency-Key: order_1042_v1
{
"chain": "DOGE",
"amount_fiat": "29.99",
"fiat_currency": "USD",
"merchant_order_id": "order_1042",
"metadata": {
"product_id": "SKU-8821",
"customer_id": "cust_77abc"
},
"customer_email": "buyer@example.com",
"success_url": "https://store.example.com/orders/1042/success",
"cancel_url": "https://store.example.com/checkout"
}
Example request — direct crypto amount with LTC:
POST /v1/payment_intents
Authorization: Bearer rp_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
{
"chain": "LTC",
"amount_doge": "0.58320000",
"merchant_order_id": "order_1043",
"success_url": "https://store.example.com/orders/1043/success",
"cancel_url": "https://store.example.com/checkout"
}
Example request — USDT TRC-20:
POST /v1/payment_intents
Authorization: Bearer rp_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
{
"chain": "USDT_TRC20",
"amount_doge": "50.00",
"merchant_order_id": "order_1044",
"success_url": "https://store.example.com/orders/1044/success",
"cancel_url": "https://store.example.com/checkout"
}
Response — 201 Created:
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"public_id": "pi_a1b2c3d4e5f6g7h8i9j0k1l2",
"client_secret": "pi_a1b2c3d4e5f6g7h8i9j0k1l2_secret_m3n4o5p6q7r8s9t0u1v2w3x4",
"status": "requires_payment",
"chain_code": "DOGE",
"chain": {
"code": "DOGE",
"name": "Dogecoin",
"symbol": "DOGE"
},
"amount_doge": "1842.57000000",
"amount_fiat": "29.99",
"fiat_currency": "USD",
"rate": {
"source": "coingecko",
"value": "0.01628",
"timestamp": "2026-03-20T10:15:00Z"
},
"address": "DMerchantWalletAddressHere12345678",
"checkout_url": "https://checkout.rexpay.co/pay/pi_a1b2c3d4e5f6g7h8i9j0k1l2_secret_m3n4o5p6q7r8s9t0u1v2w3x4",
"expires_at": "2026-03-20T10:45:00Z",
"created_at": "2026-03-20T10:15:00Z"
}
Response fields:
| Field | Type | Description |
|---|---|---|
id |
UUID | Internal UUID (for admin/DB reference) |
public_id |
string | External ID, format pi_<24 chars>. Use for API calls. |
client_secret |
string | Used by the hosted checkout UI. Do not expose server-side. |
status |
string | Always requires_payment on creation |
chain_code |
string | Selected chain code |
chain |
object | Chain metadata: code, name, symbol |
amount_doge |
decimal | Base crypto amount expected |
amount_fiat |
decimal or null | Original fiat amount requested |
fiat_currency |
string | Fiat currency code |
rate |
object or null | Exchange rate used: source, value, timestamp |
address |
string | Receiving address for this invoice |
checkout_url |
string | Hosted checkout page URL |
expires_at |
datetime | ISO 8601 expiration timestamp |
created_at |
datetime | ISO 8601 creation timestamp |
GET /v1/payment_intents/{public_id}
Retrieve full details of a PaymentIntent.
Authentication: Required (API key)
Path parameter: public_id — the pi_xxx identifier from creation.
Example request:
GET /v1/payment_intents/pi_a1b2c3d4e5f6g7h8i9j0k1l2
Authorization: Bearer rp_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Response — 200 OK:
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"public_id": "pi_a1b2c3d4e5f6g7h8i9j0k1l2",
"client_secret": "pi_a1b2c3d4e5f6g7h8i9j0k1l2_secret_m3n4o5p6q7r8s9t0u1v2w3x4",
"status": "confirmed",
"chain_code": "DOGE",
"chain": {
"code": "DOGE",
"name": "Dogecoin",
"symbol": "DOGE"
},
"amount_doge": "1842.57000000",
"amount_doge_with_salt": "1842.57000300",
"salt_applied": "0.00000300",
"amount_fiat": "29.99",
"fiat_currency": "USD",
"rate": {
"source": "coingecko",
"value": "0.01628",
"timestamp": "2026-03-20T10:15:00Z"
},
"merchant_order_id": "order_1042",
"metadata": {
"product_id": "SKU-8821",
"customer_id": "cust_77abc"
},
"address": "DMerchantWalletAddressHere12345678",
"checkout_url": "https://checkout.rexpay.co/pay/pi_a1b2c3d4e5f6g7h8i9j0k1l2_secret_...",
"confirmations_required": 1,
"expires_at": "2026-03-20T10:45:00Z",
"amount_received": "1842.57000300",
"overpayment_amount": null,
"success_url": "https://store.example.com/orders/1042/success",
"cancel_url": "https://store.example.com/checkout",
"customer_email": "buyer@example.com",
"confirmed_at": "2026-03-20T10:22:15Z",
"created_at": "2026-03-20T10:15:00Z",
"updated_at": "2026-03-20T10:22:15Z",
"transactions": [
{
"txid": "a8f3c2e1d7b6a9f0e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3",
"amount": "1842.57000300",
"confirmations": 3,
"first_seen_at": "2026-03-20T10:20:00Z",
"confirmed_at": "2026-03-20T10:22:15Z"
}
]
}
Additional response fields (vs. create response):
| Field | Type | Description |
|---|---|---|
amount_doge_with_salt |
decimal | Exact expected amount including any salt offset |
salt_applied |
decimal | Salt added (0 for XPUB mode or USDT) |
merchant_order_id |
string | Your order reference |
metadata |
object | Your custom key-value pairs |
confirmations_required |
integer | Number of confirmations needed |
amount_received |
decimal | Total crypto received across all transactions |
overpayment_amount |
decimal or null | Amount received above expected (if any) |
confirmed_at |
datetime or null | When the intent was confirmed |
transactions |
array | On-chain transactions detected |
GET /v1/payment_intents/
List PaymentIntents for your merchant account.
Authentication: Required (API key)
Query parameters:
| Parameter | Type | Description |
|---|---|---|
status |
string | Filter by status. One of: requires_payment, detected, processing, confirmed, expired, canceled, flagged |
chain_code |
string | Filter by chain: DOGE, LTC, USDT_TRC20 |
created_after |
datetime | ISO 8601. Return intents created at or after this time. |
created_before |
datetime | ISO 8601. Return intents created before or at this time. |
Example request:
GET /v1/payment_intents/?status=confirmed&chain_code=DOGE&created_after=2026-03-01T00:00:00Z
Authorization: Bearer rp_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Response — 200 OK:
[
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"public_id": "pi_a1b2c3d4e5f6g7h8i9j0k1l2",
"status": "confirmed",
"chain_code": "DOGE",
"amount_doge": "1842.57000000",
"amount_fiat": "29.99",
"fiat_currency": "USD",
"merchant_order_id": "order_1042",
"address": "DMerchantWalletAddressHere12345678",
"amount_received": "1842.57000300",
"expires_at": "2026-03-20T10:45:00Z",
"created_at": "2026-03-20T10:15:00Z"
}
]
The list response uses a lighter serializer (no transactions, client_secret, or rate detail). Paginate using standard Django REST Framework pagination if your account has many records.
POST /v1/payment_intents/{public_id}/cancel
Cancel a PaymentIntent.
Authentication: Required (API key)
Cancellation rules: A PaymentIntent can only be canceled if its status is requires_payment. Once a payment has been detected on-chain, cancellation is not possible.
Example request:
POST /v1/payment_intents/pi_a1b2c3d4e5f6g7h8i9j0k1l2/cancel
Authorization: Bearer rp_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Response — 200 OK:
Returns the full PaymentIntent object with status: "canceled".
Error — 400 Bad Request (wrong state):
{
"error": {
"type": "invalid_state",
"message": "Cannot cancel payment intent in status: detected"
}
}
GET /v1/public/checkout/{client_secret}
Retrieve checkout display data for the hosted checkout page. This endpoint is public — no API key required. The client_secret acts as the access token.
Note: Do not expose the client_secret in server-side contexts where it is unnecessary. It should only be passed to your frontend or customer-facing checkout pages.
Example request:
Response — 200 OK:
{
"status": "requires_payment",
"chain_code": "DOGE",
"chain_symbol": "DOGE",
"amount_doge": "1842.57000300",
"amount_fiat": "29.99",
"fiat_currency": "USD",
"address": "DMerchantWalletAddressHere12345678",
"expires_at": "2026-03-20T10:45:00Z",
"confirmations_required": 1,
"merchant": {
"display_name": "Example Store",
"logo_url": "https://example.com/logo.png",
"brand_color": "#FF6600",
"support_email": "support@example.com"
},
"success_url": "https://store.example.com/orders/1042/success",
"cancel_url": "https://store.example.com/checkout"
}
Response fields:
| Field | Type | Description |
|---|---|---|
status |
string | Current payment status |
chain_code |
string | Chain identifier |
chain_symbol |
string | Display symbol (e.g., DOGE, USDT) |
amount_doge |
decimal | Exact amount the customer must send (includes salt if applicable) |
amount_fiat |
decimal or null | Original fiat amount |
fiat_currency |
string | Fiat currency code |
address |
string | Receiving address |
expires_at |
datetime | Expiration time |
confirmations_required |
integer | Confirmations needed |
merchant |
object | Merchant branding data |
success_url |
string | Redirect URL on success |
cancel_url |
string | Redirect URL on cancel |
GET /v1/public/checkout/{client_secret}/status
Poll the payment status. Intended for use by the checkout page's status poller. No API key required.
Example request:
Response — 200 OK:
{
"status": "processing",
"chain_code": "DOGE",
"chain_symbol": "DOGE",
"amount_doge": "1842.57000300",
"amount_received": "1842.57000300",
"confirmations": 0,
"confirmations_required": 1,
"expires_at": "2026-03-20T10:45:00Z",
"transactions": [
{
"txid": "a8f3c2e1d7b6a9f0e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3",
"amount": "1842.57000300",
"confirmations": 0,
"first_seen_at": "2026-03-20T10:20:00Z",
"confirmed_at": null
}
]
}
Polling recommendations:
- Poll every 5–10 seconds while status is
requires_payment,detected, orprocessing. - Stop polling once status reaches a terminal state:
confirmed,expired, orcanceled. - Respect rate limits — the checkout poller is limited per IP and per
client_secret.
6. Webhook Integration
REX Pay sends signed HTTP POST requests to your webhook endpoint when payment events occur. Your endpoint must respond with any 2xx status code within 30 seconds to acknowledge receipt.
6.1 Event Types
| Event | Trigger |
|---|---|
payment_intent.created |
PaymentIntent created via API |
payment_intent.detected |
Incoming transaction seen (mempool) |
payment_intent.processing |
Transaction confirmed, accumulating confirmations |
payment_intent.confirmed |
Required confirmations reached — payment complete |
payment_intent.expired |
Invoice expired before payment received |
payment_intent.canceled |
Canceled via API |
payment_intent.flagged |
Anomalous payment (underpayment, overpayment outside tolerance, late payment) |
payment_intent.overpaid |
Customer sent more than the expected amount |
payment_intent.late_payment |
Payment detected after expiration |
payment_intent.reorged |
Block reorganization affected a confirmed transaction |
6.2 Webhook Payload
All events share the same envelope structure:
{
"id": "evt_x9y8z7w6v5u4t3s2r1q0p9o8",
"object": "event",
"type": "payment_intent.confirmed",
"created": 1711234560,
"data": {
"object": {
"id": "pi_a1b2c3d4e5f6g7h8i9j0k1l2",
"object": "payment_intent",
"status": "confirmed",
"amount_doge": "1842.57000300",
"amount_received": "1842.57000300",
"merchant_order_id": "order_1042",
"metadata": {
"product_id": "SKU-8821",
"customer_id": "cust_77abc"
},
"address": "DMerchantWalletAddressHere12345678",
"amount_fiat": "29.99",
"fiat_currency": "USD",
"created_at": "2026-03-20T10:15:00Z",
"expires_at": "2026-03-20T10:45:00Z",
"confirmed_at": "2026-03-20T10:22:15Z"
}
}
}
Envelope fields:
| Field | Type | Description |
|---|---|---|
id |
string | Event ID, format evt_<24 chars> |
object |
string | Always "event" |
type |
string | Event type (see table above) |
created |
integer | Unix timestamp of event creation |
data.object |
object | The PaymentIntent snapshot at the time of the event |
6.3 Webhook Headers
Every webhook POST includes these headers:
| Header | Description |
|---|---|
Content-Type |
application/json |
User-Agent |
RegenPay-Webhook/1.0 |
X-Webhook-Signature |
HMAC-SHA256 signature (see below) |
X-Webhook-ID |
Internal delivery UUID |
X-Event-ID |
Event ID matching payload.id |
X-Event-Type |
Event type matching payload.type |
6.4 Signature Verification
REX Pay signs every webhook using HMAC-SHA256. The X-Webhook-Signature header has the format:
Where:
- t is a Unix timestamp
- v1 is HMAC-SHA256(secret, "{timestamp}.{raw_body}")
The signed payload is constructed by concatenating the timestamp, a literal ., and the raw (unmodified) JSON request body.
Always verify signatures. Signature verification prevents replay attacks and ensures the webhook originated from REX Pay. Webhooks older than 5 minutes are rejected by the reference implementation.
Python verification:
import hashlib
import hmac
import time
def verify_webhook_signature(
payload: str,
signature_header: str,
secret: str,
tolerance_seconds: int = 300
) -> bool:
"""
Verify a REX Pay webhook signature.
Args:
payload: Raw request body as string (do not parse JSON first)
signature_header: Value of the X-Webhook-Signature header
secret: Your webhook signing secret (whsec_xxx)
tolerance_seconds: Maximum age of webhook in seconds (default 5 minutes)
Returns:
True if valid, False otherwise
"""
if not signature_header:
return False
# Parse the signature header
parts = {}
for element in signature_header.split(','):
if '=' in element:
key, value = element.split('=', 1)
parts[key] = value
timestamp_str = parts.get('t')
received_sig = parts.get('v1')
if not timestamp_str or not received_sig:
return False
try:
timestamp = int(timestamp_str)
except ValueError:
return False
# Replay protection: reject stale webhooks
if abs(int(time.time()) - timestamp) > tolerance_seconds:
return False
# Compute expected signature
signed_payload = f"{timestamp}.{payload}"
expected_sig = hmac.new(
secret.encode('utf-8'),
signed_payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
# Use constant-time comparison to prevent timing attacks
return hmac.compare_digest(expected_sig, received_sig)
# Flask example
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
@app.route('/webhooks/rexpay', methods=['POST'])
def handle_webhook():
payload = request.get_data(as_text=True)
sig_header = request.headers.get('X-Webhook-Signature', '')
if not verify_webhook_signature(payload, sig_header, WEBHOOK_SECRET):
abort(400, 'Invalid signature')
event = request.json
event_type = event['type']
if event_type == 'payment_intent.confirmed':
pi = event['data']['object']
order_id = pi['merchant_order_id']
fulfill_order(order_id)
return '', 200
Node.js verification:
const crypto = require('crypto');
/**
* Verify a REX Pay webhook signature.
* @param {string} payload - Raw request body string
* @param {string} signatureHeader - Value of X-Webhook-Signature header
* @param {string} secret - Your webhook signing secret (whsec_xxx)
* @param {number} toleranceSeconds - Max age in seconds (default 300)
* @returns {boolean}
*/
function verifyWebhookSignature(payload, signatureHeader, secret, toleranceSeconds = 300) {
if (!signatureHeader) return false;
const parts = {};
for (const element of signatureHeader.split(',')) {
const idx = element.indexOf('=');
if (idx !== -1) {
parts[element.slice(0, idx)] = element.slice(idx + 1);
}
}
const timestampStr = parts['t'];
const receivedSig = parts['v1'];
if (!timestampStr || !receivedSig) return false;
const timestamp = parseInt(timestampStr, 10);
if (isNaN(timestamp)) return false;
// Replay protection
if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > toleranceSeconds) {
return false;
}
const signedPayload = `${timestamp}.${payload}`;
const expectedSig = crypto
.createHmac('sha256', secret)
.update(signedPayload, 'utf8')
.digest('hex');
// Constant-time comparison
return crypto.timingSafeEqual(
Buffer.from(expectedSig, 'hex'),
Buffer.from(receivedSig, 'hex')
);
}
// Express example
const express = require('express');
const app = express();
const WEBHOOK_SECRET = 'whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
app.post('/webhooks/rexpay', express.raw({ type: 'application/json' }), (req, res) => {
const payload = req.body.toString('utf8');
const sigHeader = req.headers['x-webhook-signature'] || '';
if (!verifyWebhookSignature(payload, sigHeader, WEBHOOK_SECRET)) {
return res.status(400).send('Invalid signature');
}
const event = JSON.parse(payload);
if (event.type === 'payment_intent.confirmed') {
const pi = event.data.object;
fulfillOrder(pi.merchant_order_id);
}
res.status(200).send('ok');
});
PHP verification:
<?php
/**
* Verify a REX Pay webhook signature.
*
* @param string $payload Raw request body
* @param string $signatureHeader Value of X-Webhook-Signature header
* @param string $secret Your webhook signing secret (whsec_xxx)
* @param int $toleranceSecs Max age in seconds (default 300)
* @return bool
*/
function verifyWebhookSignature(
string $payload,
string $signatureHeader,
string $secret,
int $toleranceSecs = 300
): bool {
if (empty($signatureHeader)) {
return false;
}
$parts = [];
foreach (explode(',', $signatureHeader) as $element) {
if (strpos($element, '=') !== false) {
[$key, $value] = explode('=', $element, 2);
$parts[$key] = $value;
}
}
$timestampStr = $parts['t'] ?? null;
$receivedSig = $parts['v1'] ?? null;
if (!$timestampStr || !$receivedSig) {
return false;
}
$timestamp = (int) $timestampStr;
if ($timestamp === 0) {
return false;
}
// Replay protection
if (abs(time() - $timestamp) > $toleranceSecs) {
return false;
}
$signedPayload = "{$timestamp}.{$payload}";
$expectedSig = hash_hmac('sha256', $signedPayload, $secret);
// Constant-time comparison
return hash_equals($expectedSig, $receivedSig);
}
// Usage in a webhook handler
$payload = file_get_contents('php://input');
$signatureHeader = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$secret = 'whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
if (!verifyWebhookSignature($payload, $signatureHeader, $secret)) {
http_response_code(400);
echo 'Invalid signature';
exit;
}
$event = json_decode($payload, true);
if ($event['type'] === 'payment_intent.confirmed') {
$pi = $event['data']['object'];
fulfillOrder($pi['merchant_order_id']);
}
http_response_code(200);
echo 'ok';
6.5 Retry Schedule
If your endpoint fails to return a 2xx response within 30 seconds (including connection errors and timeouts), REX Pay retries delivery according to this schedule:
| Attempt | Delay (base) | Notes |
|---|---|---|
| 1 | Immediate | Initial delivery |
| 2 | 30 seconds | |
| 3 | 5 minutes | |
| 4 | 30 minutes | |
| 5 | 2 hours | |
| 6 | 5 hours | |
| 7 | 10 hours | |
| 8 | 10 hours | Final attempt |
All retry delays include +/- 25% random jitter to prevent thundering herd. After 8 failed attempts, delivery is marked permanently failed. A background reconciliation task also sweeps for overdue retries as a safety net.
Best practice: Design your webhook handler to be idempotent. Use event.id (the evt_xxx identifier) to deduplicate events — the same event may be delivered more than once if the first delivery timed out.
7. Hosted Checkout
REX Pay provides a hosted checkout page that handles QR code display, countdown timer, and status polling on your behalf.
7.1 Redirect Flow
After creating a PaymentIntent, redirect your customer to the checkout_url returned in the creation response:
The hosted checkout page:
- Displays the receiving address with a scannable QR code
- Shows the exact amount the customer must send (including any salt)
- Counts down the invoice expiration timer
- Polls the status endpoint every 5–10 seconds
- Redirects to
success_urlon confirmation orcancel_urlif the customer clicks cancel
7.2 Branding Customization
Configure the following in the merchant dashboard under Profile Settings:
| Setting | Field | Format | Description |
|---|---|---|---|
| Display name | display_name |
string (max 200) | Shown at the top of the checkout page |
| Logo | logo_url |
URL | Merchant logo displayed on checkout |
| Brand color | brand_color |
Hex color #RRGGBB |
Accent color for buttons and highlights |
| Support email | support_email |
Email address | Shown as a help contact on checkout |
7.3 Success and Cancel URL Handling
When the customer's payment is confirmed, the hosted checkout page redirects to success_url. When the customer clicks the cancel button, they are redirected to cancel_url.
No query parameters are appended to these URLs automatically. To correlate the redirect with your order, embed your order ID in the URL at PaymentIntent creation time:
success_url: "https://store.example.com/orders/1042/success"
cancel_url: "https://store.example.com/checkout?order=1042"
Do not rely solely on the redirect for order fulfillment. Redirect events can be missed if the customer closes the browser tab. Always use webhooks as the authoritative signal for order fulfillment.
8. Multi-Chain Considerations
8.1 USDT TRC-20 Differences
| Aspect | DOGE / LTC | USDT TRC-20 |
|---|---|---|
| Chain type | UTXO | Account-based (TRON token) |
| Salt support | Yes (single address mode) | No |
| Amount unit | Native coin | USDT (pegged 1:1 to USD) |
| Fiat conversion | Via CoinGecko rate | Direct (1 USDT = 1 USD) |
| Default confirmations | 1 (DOGE), 3 (LTC) | 1 |
| Finality speed | ~1 min (DOGE), ~2.5 min (LTC) | ~3 seconds per block |
| Address format | Base58 | Base58Check, 34 chars, starts with T |
| URI scheme | dogecoin:, litecoin: |
None (no standard USDT URI) |
| Minimum amount | No minimum | 0.01 USDT |
8.2 Fiat Conversion
For DOGE and LTC:
- Provide amount_fiat + fiat_currency to have REX Pay convert at the current market rate.
- The rate is fetched from CoinGecko and locked at invoice creation time.
- The rate, rate source, and rate timestamp are stored on the PaymentIntent and returned in the rate field.
- Customers send the exact converted crypto amount. No rate adjustment occurs after creation.
For USDT TRC-20:
- Provide amount_doge where the value represents the USDT amount (e.g., "amount_doge": "29.99" = 29.99 USDT).
- Alternatively, specify amount_fiat: "29.99" with fiat_currency: "USD". The system applies a 1:1 rate.
- USDT amounts use 6 decimal places on-chain. REX Pay handles precision internally.
8.3 Confirmation Requirements
| Chain | Default | Recommended minimum | Notes |
|---|---|---|---|
| DOGE | 1 | 1 for low value, 6 for high value | 1-minute block time |
| LTC | 3 | 3 for standard, 6 for high value | ~2.5-minute block time |
| USDT TRC-20 | 1 | 1 | TRON finalizes in seconds |
Override per-invoice using the confirmations field in the creation request.
8.4 Address-Only Mode Limitations
If you use single-address settlement on DOGE or LTC:
- Maximum concurrent active invoices is bounded by
salt_max_steps(default 50,000). In practice, this limit is rarely reached as invoices expire within 30 minutes. - If all salt slots are occupied,
POST /v1/payment_intentsreturns503 Service Unavailablewith error typesalt_exhausted. Retry after some invoices expire or increasesalt_max_stepsin the dashboard. - XPUB mode has no such limitation and is strongly recommended for high-volume merchants.
9. Error Handling
9.1 Error Response Format
All error responses follow this structure:
For validation errors, an additional details key contains field-level errors:
{
"error": {
"type": "validation_error",
"details": {
"amount_doge": ["Must be greater than 0"],
"fiat_currency": ["This field is required when amount_fiat is provided"]
}
}
}
9.2 HTTP Status Codes
| Status | Meaning |
|---|---|
200 OK |
Success |
201 Created |
PaymentIntent created |
400 Bad Request |
Validation error, invalid state, or missing configuration |
401 Unauthorized |
Missing or invalid API key |
403 Forbidden |
API key is revoked or merchant is inactive |
404 Not Found |
Resource does not exist or does not belong to your merchant |
429 Too Many Requests |
Rate limit exceeded |
500 Internal Server Error |
Address derivation failed (contact support) |
503 Service Unavailable |
Rate fetch failed or salt slots exhausted |
9.3 Common Error Types
| Error type | HTTP | Description |
|---|---|---|
validation_error |
400 | One or more request fields failed validation |
configuration_error |
400 | Merchant has not configured settlement for the requested chain |
invalid_state |
400 | Operation not allowed in the current status (e.g., cancel after detection) |
not_found |
404 | PaymentIntent not found or does not belong to this merchant |
rate_error |
503 | Exchange rate service unavailable; retry shortly |
salt_exhausted |
503 | All salt slots occupied; wait for invoices to expire |
address_error |
500 | Internal address derivation error; contact support |
rate_limit_exceeded |
429 | Too many requests; see Retry-After header |
9.4 Handling Rate Limit Errors
import time
import requests
def create_payment_intent_with_retry(api_key, payload, max_retries=3):
headers = {'Authorization': f'Bearer {api_key}'}
for attempt in range(max_retries):
response = requests.post(
'https://api.rexpay.co/v1/payment_intents',
json=payload,
headers=headers
)
if response.status_code == 429:
retry_after = int(response.headers.get('Retry-After', 60))
time.sleep(retry_after)
continue
return response
raise Exception('Max retries exceeded')
10. Testing
10.1 Test Mode
Use your test API key (rp_test_xxx) to work in test mode. Test mode:
- Routes to testnet addresses (DOGE testnet, LTC testnet, TRON Nile testnet)
- Accepts testnet coins only
- Uses the same API surface and response format as live mode
- Is clearly indicated by the
rp_test_prefix on the key
Test mode invoices will not receive real payments. Use faucets to obtain testnet coins for manual testing.
10.2 Test Faucets
| Chain | Testnet | Faucet |
|---|---|---|
| DOGE | Dogecoin testnet | https://doge-faucet-testnet.bitaps.com/ |
| LTC | Litecoin testnet | https://ltc.bitaps.com/ (check for testnet option) |
| USDT | TRON Nile testnet | https://nileex.io/join/getJoinPage |
10.3 Test Address Formats
Configure your test settlement with testnet XPUBs or testnet addresses:
| Chain | Testnet address prefix |
|---|---|
| DOGE testnet | n |
| LTC testnet | m, n, tltc1 |
| TRON Nile | T (same prefix as mainnet) |
10.4 API Key Summary
| Key prefix | Mode | Chains |
|---|---|---|
rp_test_ |
Test | Testnet |
rp_live_ |
Live | Mainnet |
Never use a live key in development or CI environments. Never commit API keys to source control. Use environment variables or a secrets manager.
10.5 Quick Test — Create and Inspect
# Create a test PaymentIntent
curl -X POST https://api.rexpay.co/v1/payment_intents \
-H "Authorization: Bearer rp_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"chain": "DOGE",
"amount_fiat": "10.00",
"fiat_currency": "USD",
"merchant_order_id": "test_order_001"
}'
# Retrieve it
curl https://api.rexpay.co/v1/payment_intents/pi_<returned_id> \
-H "Authorization: Bearer rp_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# List recent intents
curl "https://api.rexpay.co/v1/payment_intents/?status=requires_payment" \
-H "Authorization: Bearer rp_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# Cancel it
curl -X POST https://api.rexpay.co/v1/payment_intents/pi_<returned_id>/cancel \
-H "Authorization: Bearer rp_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
For support or integration questions, contact the REX Pay team via the dashboard or at support@rexpay.co.