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
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
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.
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).
{
"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
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:
https://demo.rsends.io/pay/pi_9f1c4a7e2b5d8036c1e94ab7d2508f63Open 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.
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"]
}'{
"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.
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
Where to go next
| If you need | Read |
|---|---|
| Every request field, the recipient rules, the fee model | Payment intents |
| Keys, scopes, the approval gate, idempotency | Authentication |
| Every event, the payload contract, retries | Webhooks |
| Error codes and rate limits | Errors |