thru

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.

bash
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)

bash
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:

json
{
  "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

bash
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"}}'
json
{
  "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
}
  • paymentAddress is a fresh address created for this payment only.
  • amount and expectedAmount are in token units: "1" is 1 USDC.
  • expiresAt is 30 minutes after creation.
  • Sending the same request again with the same idempotencyKey returns 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.

bash
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:

json
{
  "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:

json
{
  "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:

bash
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-chains and going-live.