Appearance
Collect stablecoin payments
Get paid in stablecoins such as USDT and USDC with Payd Stables Checkout. You create a checkout for an amount in US dollars. Your customer opens the link, picks a network and a coin, and sends the payment from their wallet. Payd confirms it, credits your Payd balance and calls your callback_url.
POST
https://stables.paydlabs.com/api/v1/checkoutHow it works
- You create a checkout with the amount in USD and a
callback_url. - You send the customer to
checkout_url. - The customer picks a network and a coin. The page shows a deposit address and a QR code.
- The customer sends the exact amount within 30 minutes.
- Payd waits for the network to confirm the transfer, credits your Payd balance and posts a signed callback.
Stables Checkout uses its own host, stables.paydlabs.com, and the same API username and password as the rest of the Payd API.
Networks and coins
| Network | Coins |
|---|---|
| Base | USDC |
| Solana | USDT, USDC |
| Celo | cUSD, USDT, USDC |
| Polygon | USDC, USDT |
| BNB Smart Chain | USDT, USDC, BUSD |
| Tron | USDT |
| Arbitrum | USDC |
| Optimism | USDC |
| Ethereum | USDC, USDT |
| Avalanche | USDT, USDC |
| Stellar | USDC |
The checkout page shows the networks open on your account. Your customer must send the coin on the network they picked: the same coin on another network does not arrive.
1. Create a checkout
bash
curl https://stables.paydlabs.com/api/v1/checkout \
-u "$PAYD_API_USERNAME:$PAYD_API_PASSWORD" \
-H "Content-Type: application/json" \
-d '{
"amount": 49.99,
"payment_method": "stables",
"callback_url": "https://example.com/webhooks/payd-stables",
"redirect_url": "https://example.com/orders/8842",
"email": "amara@example.com",
"first_name": "Amara",
"last_name": "Okafor",
"reason": "Order 8842"
}'js
const auth = Buffer.from(
`${process.env.PAYD_API_USERNAME}:${process.env.PAYD_API_PASSWORD}`,
).toString('base64')
const res = await fetch('https://stables.paydlabs.com/api/v1/checkout', {
method: 'POST',
headers: { Authorization: `Basic ${auth}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
amount: 49.99,
payment_method: 'stables',
callback_url: 'https://example.com/webhooks/payd-stables',
redirect_url: 'https://example.com/orders/8842',
email: 'amara@example.com',
reason: 'Order 8842',
}),
signal: AbortSignal.timeout(30_000),
})
const checkout = await res.json()
if (!res.ok || !checkout.success) throw new Error(checkout.message)
// Store the reference with your order, then redirect the customer
saveOrderReference('8842', checkout.transaction_reference)
redirect(checkout.checkout_url)python
import os
import requests
res = requests.post(
"https://stables.paydlabs.com/api/v1/checkout",
auth=(os.environ["PAYD_API_USERNAME"], os.environ["PAYD_API_PASSWORD"]),
json={
"amount": 49.99,
"payment_method": "stables",
"callback_url": "https://example.com/webhooks/payd-stables",
"redirect_url": "https://example.com/orders/8842",
"email": "amara@example.com",
"reason": "Order 8842",
},
timeout=30,
)
res.raise_for_status()
checkout = res.json()
# Store the reference with your order, then redirect the customer
save_order_reference("8842", checkout["transaction_reference"])
redirect(checkout["checkout_url"])Fields
| Field | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Amount in US dollars, more than 0 and up to 1,000,000. |
payment_method | string | Yes | stables. |
callback_url | string | Yes | Where Payd posts the result. |
redirect_url | string | No | Where the page sends the customer after they pay. |
email | string | No | The customer's email. |
first_name, last_name | string | No | The customer's name. |
phone | string | No | The customer's phone number. |
reason | string | No | What the payment is for, up to 500 characters. The customer sees it. |
Response
json
{
"success": true,
"status": "success",
"message": "Payment request sent successfully",
"payment_method": "stables",
"checkout_url": "https://stables.paydlabs.com/pay/0b6f3c1e-8a2d-4f5b-9c7e-1d3a5b7c9e2f",
"transaction_reference": "PLS4F9A1C2BeST"
}Store transaction_reference. It ends in eST and is how you look the payment up and match the callback to your order.
2. Get the result
Payd posts to your callback_url when the payment completes, or when the checkout expires without the full amount:
json
{
"transaction_reference": "PLS4F9A1C2BeST",
"result_code": 0,
"success": true,
"amount": 49.99,
"amount_requested": 49.99,
"amount_received": 49.99,
"bank_name": "USDC",
"channel": "base",
"web3_transaction_reference": "0x8f2a...c41d",
"transaction_date": "2026-10-16 11:42:03.512204 +0000 UTC",
"customer_name": "Amara Okafor",
"forward_url": "https://example.com/webhooks/payd-stables"
}| Field | Meaning |
|---|---|
transaction_reference | The reference from the checkout. |
result_code | 0 when the payment completed, 1 when the checkout expired. |
success | true when the payment completed. |
amount | What Payd credited to your balance, in USD. Never more than you asked for. |
amount_requested, amount_received | What you asked for and what arrived, in USD. |
overpayment | Only when the customer sent too much: the extra amount. |
bank_name | The coin the customer paid with, such as USDC. |
channel | The network, such as base. |
web3_transaction_reference | The on-chain transaction hash. Use it to look the transfer up in a block explorer. |
transaction_date | When Payd sent the callback, such as 2026-10-16 11:42:03.512204 +0000 UTC. |
The body has other fields. Ignore them.
Verify the callback
Every callback has an X-Payd-Stables-Signature header: the hex HMAC-SHA256 of the exact body, keyed with your Stables webhook secret. Check it before you trust the callback.
js
import crypto from 'node:crypto'
export function verifyStables(rawBody, signature, secret) {
if (!signature) return false
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex')
return signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
}
// with express.raw({ type: 'application/json' }) on the route:
// verifyStables(req.body, req.get('X-Payd-Stables-Signature'), process.env.PAYD_STABLES_WEBHOOK_SECRET)python
import hashlib
import hmac
def verify_stables(raw_body: bytes, signature: str | None, secret: str) -> bool:
if not signature:
return False
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(signature, expected)Get the secret once and keep it with your other secrets:
bash
curl https://stables.paydlabs.com/api/v1/webhook-secret \
-u "$PAYD_API_USERNAME:$PAYD_API_PASSWORD"The secret is in callback_secret. To change it, call POST /api/v1/webhook-secret/rotation: the new secret comes back in pending_callback_secret, and the old one keeps signing. Deploy the new secret, then call POST /api/v1/webhook-secret/rotation/activate. Accept either secret for a few minutes around the switch. DELETE /api/v1/webhook-secret/rotation cancels a rotation you have not activated.
Delivery
- Answer
200,201or202within 15 seconds. Any other answer counts as a failure. - After a failure Payd tries again after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours, then stops.
- Make your handler safe to run twice: skip a
transaction_referenceyou have already handled.
3. Check a payment
GET
https://stables.paydlabs.com/api/v1/checkout/{transaction_reference}bash
curl https://stables.paydlabs.com/api/v1/checkout/PLS4F9A1C2BeST \
-u "$PAYD_API_USERNAME:$PAYD_API_PASSWORD"json
{
"reference": "PLS4F9A1C2BeST",
"status": "completed",
"amount_requested": 49.99,
"amount_received": 49.99,
"currency": "USD",
"asset": "USDC",
"chain": "base",
"deposit_address": "0x3c9e...7a1b",
"payer_email": "amara@example.com",
"expires_at": "2026-10-16T12:12:03Z",
"completed_at": "2026-10-16T11:42:03Z",
"created_at": "2026-10-16T11:40:51Z"
}To list your checkouts, call GET /api/v1/checkout?page=1&page_size=20&status=completed. page_size is 20 by default and up to 100.
Statuses
status | Meaning |
|---|---|
pending | The checkout exists. The customer has not picked a network yet. |
awaiting_payment | The customer has a deposit address. Payd is waiting for the transfer. |
confirming | The transfer is on the network. Payd is waiting for confirmations. |
completed | Paid. Your Payd balance is credited. |
underpaid | Less than the amount arrived. Payd waits for the rest until the checkout expires. |
overpaid | More than the amount arrived. Payd credits the amount you asked for. Contact support to return the rest. |
expired | 30 minutes passed without the full amount. If part of it arrived, contact support. |
failed | The payment did not go through. |
Payd allows a difference of up to $0.01 between the amount asked and the amount received.
Tips
- Tell your customer which network and coin they picked, and to send from a wallet that supports it.
- Ask them to send the exact amount in one transfer, and to allow for the network fee on top.
- Do not ship on the redirect alone. Wait for the callback, or check the status.

