StatusPrivate betaPublic testnets — test tokens, no real valueMainnet rolls out per jurisdiction

Get started

Quickstart

Six steps from nothing to a payment that has actually settled on-chain, in the sandbox (Base Sepolia). Everything here is real and copyable — the only thing you must replace is your own key.

What you need first

  • An RSends account with its company profile submitted — one self-serve form, no waiting on anyone (see step 1).
  • A wallet you control, funded with Base Sepolia test USDC and a little test ETH for gas. This is the payer wallet — see Testing.
  • A destination wallet address for the funds to land in.
  • A server that can receive an HTTPS webhook (or a tunnel to your laptop).

Sandbox base URL

Every call below goes to https://pay.rsends.io/api/backend, followed by the endpoint path. So /api/v1/merchant/payment-intent is reached at https://pay.rsends.io/api/backend/api/v1/merchant/payment-intent. See Environments for why the base URL carries that prefix today.

1. Get a sandbox API key

Keys are self-serve, and so is everything before them. Sign up, submit your company profile when /app asks for it, then open API keys → Merchant keys and create one. You need the admin role in your organisation. The key is rsend_test_…, scoped write, bound to the test environment, and shown once.

No approval queue for the sandbox

Submitting the company profile is the only gate — you are not waiting on anyone to let you in. If every call returns 403 with {"detail":{"code":"company_profile_required"}}, the profile is still incomplete; finish it in /app and the same call works immediately. (Mainnet is different: a rsend_live_ key does require manual approval.)

2. Create a payment intent

Ask for an amount, in a token, on a chain. The amount is denominated in the settlement token — 100 means 100 USDC.

POST /api/v1/merchant/payment-intent
curl -X POST https://pay.rsends.io/api/backend/api/v1/merchant/payment-intent \
  -H "Authorization: Bearer rsend_test_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: order-1024-attempt-1" \
  -d '{
    "amount": 100,
    "currency": "USDC",
    "chain": "base_sepolia",
    "recipient": "0xYourDestinationWallet00000000000000000000",
    "expires_in_minutes": 30,
    "metadata": { "order_id": "ORD-1024" }
  }'

200 OK. The response echoes the intent and adds an onchain object — the exact instructions the payer's wallet will execute. Two fields matter immediately: intent_id (step 3) and onchain.total (what the payer actually authorises).

200 application/json (abridged)
{
  "intent_id": "pi_9f1c4a7e2b5d8036c1e94ab7d2508f63",
  "reference_id": "a1b2c3d4e5f6a7b8",
  "onchain_invoice_id": "0x9f…",
  "amount": 100.0,
  "currency": "USDC",
  "chain": "base_sepolia",
  "recipient": "0xyourdestinationwallet00000000000000000000",
  "status": "pending",
  "expires_at": "2026-08-06T12:30:00+00:00",
  "created_at": "2026-08-06T12:00:00+00:00",
  "onchain": {
    "invoiceId": "0x9f…",
    "merchant":  "0xyourdestinationwallet00000000000000000000",
    "token":     "0x036cbd53842c5426634e7929541ec2318f3dcf7e",
    "amount":    "100000000",      // 100 USDC — what YOU receive
    "fee":       "600000",         // 0.60 USDC — live from the router
    "total":     "100600000",      // what the PAYER parts with
    "maxFee":    "600000",
    "chainId":   84532,
    "router":    "0x2Ec353815F2Cd382628d0D399F8d80959C1758CA",
    "routerVersion": 1,
    "function":  "pay",
    "decimals":  6,
    "isNative":  false
  }
}

On the sandbox the payer pays amount + fee

The deployed Base Sepolia router is routerVersion 1, which charges an on-chain flat fee on top of your amount: the payer authorises total, you receive amount whole. At the mainnet cutover this becomes the fee-less routerVersion 2 — fee is "0" and total == amount. Branch on routerVersion, never on a hardcoded assumption. See Payment intents.

3. Send the payer to the hosted checkout

There is no checkout_url field — you compose the URL from the intent_id:

checkout URL
https://demo.rsends.io/pay/pi_9f1c4a7e2b5d8036c1e94ab7d2508f63

Open it, connect the funded wallet, approve, and pay. The stablecoin moves from the payer's wallet to your destination wallet in one transaction. Nothing passes through RSends. If you'd rather build your own checkout, everything you need is in the onchain object — see Hosted checkout.

4. Register a webhook

Do this before the payment if you want to watch it land live. The URL must be HTTPS and publicly resolvable — private, loopback and link-local addresses are rejected with 422 WEBHOOK_URL_FORBIDDEN.

POST /api/v1/merchant/webhook/register
curl -X POST https://pay.rsends.io/api/backend/api/v1/merchant/webhook/register \
  -H "Authorization: Bearer rsend_test_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.example.com/rsends/webhook",
    "events": ["payment.completed", "payment.expired"]
  }'
200 application/json
{
  "webhook_id": 42,
  "url": "https://your-app.example.com/rsends/webhook",
  "secret": "…64 hex chars…",
  "events": ["payment.completed", "payment.expired"],
  "is_active": true
}

Store the secret now

secret is returned in this response and never again. It is the HMAC key for every delivery's signature. Fire a test delivery with POST /api/v1/merchant/webhook/test ({"webhook_id": 42}) to confirm your endpoint and verification work before a real payment arrives.

5. Verify the webhook, then fulfil

When the payment settles you receive payment.completed. Verify the signature over the raw body before reading a single field.

verify a delivery — Node.js
import crypto from 'node:crypto'

function verifyRSendsWebhook(secret, headers, rawBody) {
  const ts  = headers['x-rsend-timestamp']
  const sig = headers['x-rsend-signature']

  // Freshness: reject anything outside a 5-minute window (anti-replay).
  const now = Math.floor(Date.now() / 1000)
  if (!ts || Math.abs(now - Number(ts)) > 300) return false

  // HMAC-SHA256 over "{timestamp}.{rawBody}", constant-time compare.
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${ts}.${rawBody}`)
    .digest('hex')

  return sig.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))
}

Dedupe on X-RSend-Delivery-Id — a retry carries the same id, and RSends retries up to 5 times on any non-2xx. Full payload shape, event list and retry schedule: Webhooks.

Prefer polling? GET /api/v1/merchant/payment-intent/{intent_id} returns the same state (60/min per key).

6. Confirm it on-chain

Take tx_hash from the webhook to sepolia.basescan.org and read the PaymentMade event: the amount, your wallet, the payer. You have now verified the payment without trusting RSends at all.

That’s the whole integration

One POST to create, one redirect, one signed webhook to verify. No balance to reconcile, no payout to wait for, no funds held anywhere. Going to production means swapping the key and the chain — see Testing.

Where to go next

If you needRead
Every request field, the recipient rules, the fee modelPayment intents
Keys, scopes, the approval gate, idempotencyAuthentication
Every event, the payload contract, retriesWebhooks
Error codes and rate limitsErrors