---
title: "Quickstart: your first testnet payment"
description: "Create a key, register a webhook, create a 1 USDC payment on Arc testnet, pay it, and receive a signed payment.confirmed event, all with curl"
source: https://docs.thru.la/quickstart.md
html: https://docs.thru.la/quickstart
index: https://docs.thru.la/llms.txt
---
# 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`.

---

Related: [auth](https://docs.thru.la/auth.md) · [payments](https://docs.thru.la/payments.md) · [webhooks](https://docs.thru.la/webhooks.md) · [supported-chains](https://docs.thru.la/supported-chains.md)
