Skip to content

REX Pay Merchant Integration Guide

Version: 1.0 Base URL: https://api.rexpay.co Dashboard: https://app.rexpay.co


Table of Contents

  1. Overview
  2. Getting Started
  3. Settlement Configuration
  4. Payment Flow
  5. API Reference
  6. Webhook Integration
  7. Hosted Checkout
  8. Multi-Chain Considerations
  9. Error Handling
  10. 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:

Authorization: Bearer rp_live_abc123...

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):

  1. Navigate to the Dogecoin account (m/44'/3'/0')
  2. Export the account-level extended public key
  3. The key begins with dgub (mainnet) or tgub (testnet)
  4. 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

  1. Create PaymentIntent — Your backend calls POST /v1/payment_intents with the amount and chain. REX Pay generates a unique receiving address and returns a checkout_url.
  2. Redirect Customer — Send your customer to the hosted checkout URL, or use the address and amount_doge fields to build your own payment UI.
  3. Customer Pays — The customer sends crypto to the displayed address and exact amount.
  4. Detection — REX Pay's blockchain monitor detects the incoming transaction (status transitions to detected, then processing).
  5. Confirmation — After the required number of block confirmations, the intent status transitions to confirmed.
  6. Webhook — REX Pay fires a payment_intent.confirmed webhook to your registered endpoint.
  7. 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:

Authorization: Bearer rp_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

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:

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 58
X-RateLimit-Reset: 1711234560

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.

Idempotency-Key: order_9283_attempt_1

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:

GET /v1/public/checkout/pi_a1b2c3d4e5f6g7h8i9j0k1l2_secret_m3n4o5p6q7r8s9t0u1v2w3x4

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:

GET /v1/public/checkout/pi_a1b2c3d4e5f6g7h8i9j0k1l2_secret_m3n4o5p6q7r8s9t0u1v2w3x4/status

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, or processing.
  • Stop polling once status reaches a terminal state: confirmed, expired, or canceled.
  • 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:

t=1711234560,v1=a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4

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:

https://checkout.rexpay.co/pay/{client_secret}

The hosted checkout page:

  1. Displays the receiving address with a scannable QR code
  2. Shows the exact amount the customer must send (including any salt)
  3. Counts down the invoice expiration timer
  4. Polls the status endpoint every 5–10 seconds
  5. Redirects to success_url on confirmation or cancel_url if 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_intents returns 503 Service Unavailable with error type salt_exhausted. Retry after some invoices expire or increase salt_max_steps in 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:

{
  "error": {
    "type": "error_type",
    "message": "Human-readable description"
  }
}

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.