Quickstart
From sign-up to a paid test invoice, with a test key and no real money.
By the end of this page you will have created an invoice, opened its checkout, paid it in test mode, and read it back as paid. It takes a few minutes and moves no money.
Get a test key
Sign up in the dashboard. Volume emails a six-digit code to prove the address, then you choose your business name and a password. Test mode works at once, while Volume reviews your business for live payments.
Open Developers, name a key under API keys, choose Test, and create it.
Copy the vk_test_… key now: Volume shows a key only once. It goes in the Authorization header of every
request:
export VOLUME_KEY="vk_test_..."
curl https://api.paywithvolume.com/v1/account \
-H "Authorization: Bearer $VOLUME_KEY"{
"object": "account",
"id": "mer_8Kq2VbN4xT7pLm3RcY6dWs1A",
"business_name": "Monierate",
"legal_name": null,
"status": "pending",
"live_access": false,
"fee_bps": 100,
"activated_at": null,
"checkout_theme": "dark",
"created_at": "2026-10-15T09:12:44.000Z"
}pending with live_access: false is where every account starts. Everything on this
page works in that state.
Create an invoice
An invoice is what the payer owes, in US dollars. The amount is a string, never a number:
curl https://api.paywithvolume.com/v1/invoices \
-H "Authorization: Bearer $VOLUME_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"amount": "25.00",
"description": "Monierate wallet top-up",
"customer_email": "ada@example.com"
}'{
"object": "invoice",
"id": "inv_3Hf8QwZ1mKp7XnB2sLd9Vt4R",
"livemode": false,
"status": "open",
"amount": "25.00",
"currency": "usd",
"description": "Monierate wallet top-up",
"customer": "cus_7Rt2LmQ9xWb4Ks1NvP8dHz3F",
"customer_email": "ada@example.com",
"metadata": {},
"payments": [],
"paid_at": null,
"created_at": "2026-10-15T09:14:02.000Z"
}Every POST that creates something takes an Idempotency-Key. Retrying with the
same key returns the first response instead of creating a second invoice. See
idempotency.
Open a checkout
A checkout session is the page the payer pays on. Give it the invoice, and where to send the payer afterwards:
curl https://api.paywithvolume.com/v1/checkout_sessions \
-H "Authorization: Bearer $VOLUME_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"invoice_id": "inv_3Hf8QwZ1mKp7XnB2sLd9Vt4R",
"success_url": "https://example.com/paid",
"cancel_url": "https://example.com/cancelled"
}'{
"object": "checkout_session",
"id": "cs_9Wd4Tn2KxR7bLq1MhV5sZp8C",
"livemode": false,
"url": "https://paywithvolume.com/cs_9Wd4Tn2KxR7bLq1MhV5sZp8C",
"status": "open",
"invoice": "inv_3Hf8QwZ1mKp7XnB2sLd9Vt4R",
"customer": "cus_7Rt2LmQ9xWb4Ks1NvP8dHz3F",
"customer_email": "ada@example.com",
"amount": "25.00",
"currency": "usd",
"description": "Monierate wallet top-up",
"payment": null,
"amount_received": null,
"payments": [],
"success_url": "https://example.com/paid",
"cancel_url": "https://example.com/cancelled",
"expires_at": "2026-10-16T09:15:30.000Z",
"paid_at": null,
"created_at": "2026-10-15T09:15:30.000Z"
}The checkout is open for 24 hours. Once the payer picks a coin, they have 30 minutes to send it.
Pay it, without paying it
Open the url in a browser. Pick Binance, then Tether USD: the page shows a
deposit address on BNB Smart Chain, Binance's cheapest network for it, and the exact
amount to withdraw.
A test checkout also shows a Test mode panel under it. Press Send the exact amount. The page shows the payment on its way, and about ten seconds later the receipt. Send too little and Send too much show what a payer's mistakes look like.
To pay from code instead, call the test helper once a coin has been chosen:
curl -X POST \
https://api.paywithvolume.com/v1/test_helpers/checkout_sessions/cs_9Wd4Tn2KxR7bLq1MhV5sZp8C/pay \
-H "Authorization: Bearer $VOLUME_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{ "outcome": "exact" }'Either way the payment goes through the same steps as a real one, so your webhooks and
your balance show what they would in live mode. A live key is refused with
403 test_mode_only.
Read it back
curl https://api.paywithvolume.com/v1/invoices/inv_3Hf8QwZ1mKp7XnB2sLd9Vt4R \
-H "Authorization: Bearer $VOLUME_KEY"The invoice is now paid, and payments lists the transfer that paid it, confirmed:
{
"object": "invoice",
"id": "inv_3Hf8QwZ1mKp7XnB2sLd9Vt4R",
"status": "paid",
"amount": "25.00",
"payments": [
{
"object": "payment",
"id": "pay_5Np1Xc8VqL3tRz7KbM2wHd6J",
"checkout_session": "cs_9Wd4Tn2KxR7bLq1MhV5sZp8C",
"asset": "USDT",
"network": "bsc",
"amount": "25.000000",
"status": "confirmed",
"tx_hash": "fcd3fcf3893605287a54c62e3923958dc854f3bad53754cff331cfd69510ecf5",
"detected_at": "2026-10-15T09:17:05.000Z",
"confirmed_at": "2026-10-15T09:17:15.000Z"
}
],
"paid_at": "2026-10-15T09:17:15.000Z"
}The response above leaves out fields that did not change. The payer's receipt has a
button back to your success_url.
What to build next
Polling is fine for a first look. A real integration waits for Volume to tell it.
Fund a wallet: Monierate
The whole integration: start a checkout, take the webhook, credit once.
Webhooks
Register an endpoint, check signatures, and handle each event.
How a payment moves
Every status a checkout passes through, and what a payer's mistakes do.
Test mode
Short and over payments, test webhooks, and what never happens in test.