Quickstart: your first testnet payment
This takes about 15 minutes. You create a payment for 1 test USDC, pay it from a wallet, and watch thru confirm it and call your webhook. It uses the direct Payments API because it has the fewest moving parts. Every other integration (checkout sessions, payment links, invoices) creates the same kind of payment underneath.
The example uses Arc testnet, because it is the testnet that api.thru.la watches today (see supported-chains). On Arc, USDC is the chain's native asset, and a transfer confirms after one block.
Before you start
- A thru console account.
- A wallet connected to Arc Testnet, holding some test USDC from Circle's testnet faucet.
- Optional: an HTTPS URL that can receive POST requests, for the webhook. A request-bin service or a local tunnel works. Without one you can still follow the payment by polling.
1. Create an API key
In the console, open Developers → API keys and create a key. It is shown once, so copy it now.
export THRU_API_KEY=thru_sk_...
Keys can only be created by a signed-in person in the console; an API key cannot create another key.
2. Register a webhook endpoint (optional)
curl -X POST https://api.thru.la/v1/webhooks \
-H "x-api-key: $THRU_API_KEY" \
-H "content-type: application/json" \
-d '{"url":"https://example.com/webhooks/thru","eventTypes":["payment.*"],"description":"Payments"}'
The response is the endpoint, including its signing secret:
{
"id": "00000000-0000-4000-8000-0000000000c1",
"merchantId": "00000000-0000-4000-8000-000000000001",
"url": "https://example.com/webhooks/thru",
"secret": "<64 lowercase hex characters>",
"enabled": true,
"eventTypes": [
"payment.*"
],
"description": "Payments",
"lastDeliveryAt": null,
"lastDeliveryStatus": null,
"createdAt": "2026-09-19T08:00:00.000Z",
"updatedAt": "2026-09-19T08:00:00.000Z"
}
Save secret; you need it to verify deliveries. "eventTypes": ["payment.*"] subscribes this endpoint to the payment events. An empty list would subscribe it to everything.
3. Create a payment
curl -X POST https://api.thru.la/v1/payments \
-H "x-api-key: $THRU_API_KEY" \
-H "content-type: application/json" \
-d '{"chain":"arc","network":"testnet","token":"USDC","amount":"1","currency":"USD","idempotencyKey":"order_1042","metadata":{"orderId":"1042"}}'
{
"id": "00000000-0000-4000-8000-0000000000a1",
"merchantId": "00000000-0000-4000-8000-000000000001",
"chain": "arc",
"network": "testnet",
"token": "USDC",
"amount": "1",
"currency": "USD",
"expectedAmount": "1",
"receivedAmount": "0",
"feeBps": 0,
"feeAmount": "0",
"paymentAddress": "0x3f5c…a91e",
"status": "waiting_for_payment",
"idempotencyKey": "order_1042",
"metadata": {
"orderId": "1042"
},
"productId": null,
"expiresAt": "2026-09-19T08:30:00.000Z",
"createdAt": "2026-09-19T08:00:00.000Z",
"updatedAt": "2026-09-19T08:00:00.000Z",
"confirmedAt": null
}
paymentAddressis a fresh address created for this payment only.amountandexpectedAmountare in token units:"1"is 1 USDC.expiresAtis 30 minutes after creation.- Sending the same request again with the same
idempotencyKeyreturns this same payment instead of creating a second one.
4. Pay it
From your wallet, send exactly 1 USDC on Arc Testnet to paymentAddress, as an ordinary transfer of the native asset. A transfer made from inside a smart contract is not detected, and that includes a transfer through USDC's ERC-20 interface at 0x3600…0000.
5. Watch the status
This endpoint needs no key; the payment id is the credential. A custom payment screen can poll it every few seconds.
curl https://api.thru.la/v1/public/payments/00000000-0000-4000-8000-0000000000a1
A few seconds after your transfer is included in a block, status moves from waiting_for_payment to confirmed:
{
"id": "00000000-0000-4000-8000-0000000000a1",
"chain": "arc",
"network": "testnet",
"token": "USDC",
"currency": "USD",
"expectedAmount": "1",
"receivedAmount": "1",
"paymentAddress": "0x3f5c…a91e",
"status": "confirmed",
"expiresAt": "2026-09-19T08:30:00.000Z",
"createdAt": "2026-09-19T08:00:00.000Z",
"confirmedAt": "2026-09-19T08:03:12.000Z",
"transactions": [
{
"id": "00000000-0000-4000-8000-0000000000b1",
"txHash": "0x8b2e…41d7",
"amount": "1",
"confirmations": 1,
"status": "confirmed",
"createdAt": "2026-09-19T08:03:12.000Z"
}
]
}
6. Receive the webhook
Within about a minute, thru POSTs this body to your endpoint, with an x-thru-signature header:
{
"id": "00000000-0000-4000-8000-0000000000d1",
"type": "payment.confirmed",
"createdAt": "2026-09-19T08:03:12.000Z",
"data": {
"merchantId": "00000000-0000-4000-8000-000000000001",
"eventType": "payment.confirmed",
"payment": {
"id": "00000000-0000-4000-8000-0000000000a1",
"merchantId": "00000000-0000-4000-8000-000000000001",
"chain": "arc",
"network": "testnet",
"token": "USDC",
"amount": "1",
"currency": "USD",
"expectedAmount": "1",
"receivedAmount": "1",
"feeBps": 0,
"feeAmount": "0",
"paymentAddress": "0x3f5c…a91e",
"status": "confirmed",
"idempotencyKey": "order_1042",
"metadata": {
"orderId": "1042"
},
"productId": null,
"expiresAt": "2026-09-19T08:30:00.000Z",
"createdAt": "2026-09-19T08:00:00.000Z",
"updatedAt": "2026-09-19T08:03:12.000Z",
"confirmedAt": "2026-09-19T08:03:12.000Z"
},
"blockchainTransaction": {
"id": "00000000-0000-4000-8000-0000000000b1",
"paymentId": "00000000-0000-4000-8000-0000000000a1",
"merchantId": "00000000-0000-4000-8000-000000000001",
"chain": "arc",
"network": "testnet",
"txHash": "0x8b2e…41d7",
"logIndex": 0,
"fromAddress": "0x92c4…07fa",
"toAddress": "0x3f5c…a91e",
"tokenAddress": null,
"amount": "1",
"blockNumber": "18204417",
"confirmations": 1,
"status": "confirmed",
"createdAt": "2026-09-19T08:03:12.000Z",
"updatedAt": "2026-09-19T08:03:12.000Z"
}
}
}
Before you trust a delivery, verify the signature over the raw request body. The webhooks topic has the code for Node, Go and Python, and a test vector.
7. Read the authoritative state
A webhook tells you something changed. Before you fulfil an order, read the payment with your key and check it:
curl https://api.thru.la/v1/payments/00000000-0000-4000-8000-0000000000a1 \
-H "x-api-key: $THRU_API_KEY"
Fulfil only if network is the network you expected, status is confirmed or overpaid (or underpaid, if you accept partial payments), and receivedAmount covers what you are delivering.
Next steps
- Hosted checkout, for fixed-price items and for amounts your server sets per purchase:
checkout-sessions. - Credit top-ups on the hosted page, credited from what arrived:
credit-topups. - Everything about deliveries, retries and replays:
webhooks. - Before accepting real money:
supported-chainsandgoing-live.