Skip to main content
This guide walks you through accepting your first crypto payment. By the end, you will have configured a payment method, created a checkout, and handled the payment confirmation.
You need a billing.io account with an API key. Use sandbox keys (sk_test_) to test without real funds. See Sandbox Testing to get started.
  1. Create a payment method
  2. Create a checkout
  3. Handle the checkout.completed webhook
  4. Unlock access in your app
Everything else in the docs is optional and can be added later.

Step 1: Create a Payment Method

A payment method defines how you receive crypto. You must create at least one before you can accept payments.
1

Register your wallet

Add a wallet address to your organization through the dashboard at Settings > Wallets, or use the wallets API. Note the returned wallet_id.
2

Create a payment method

Attach a chain + token pair to your wallet. This becomes the receiving rail for checkouts.
curl -X POST https://api.billing.io/v1/payment-methods \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "wallet_id": "wal_a1b2c3d4e5f6",
    "chain": "tron",
    "token": "USDT",
    "display_name": "USDT on Tron (Primary)",
    "is_default": true
  }'
The response returns a payment method object:
{
  "id": "pm_8f3a1b2c4d5e6f7a",
  "org_id": "org_...",
  "wallet_id": "wal_a1b2c3d4e5f6",
  "chain": "tron",
  "token": "USDT",
  "display_name": "USDT on Tron (Primary)",
  "status": "active",
  "is_default": true,
  "min_amount_usd": null,
  "max_amount_usd": null,
  "environment": "live",
  "created_at": "2025-06-15T10:30:00Z",
  "updated_at": "2025-06-15T10:30:00Z"
}
billing.io applies safe confirmation defaults per chain. See Payment Methods for details on supported chains and advanced configuration.

Step 2: Create a Checkout

A checkout is a single payment session. It generates a deposit address that your customer sends crypto to. Checkouts expire after a configurable TTL (default: 30 minutes).
curl -X POST https://api.billing.io/v1/checkouts \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
  -d '{
    "amount_usd": 49.99,
    "chain": "tron",
    "token": "USDT",
    "expires_in_seconds": 1800,
    "metadata": {
      "order_id": "ord_12345",
      "customer_email": "user@example.com"
    }
  }'
The API returns the full checkout object including a deposit_address for the customer to pay:
{
  "checkout_id": "co_9a8b7c6d5e4f",
  "deposit_address": "TXrkA1b2C3d4E5f6G7h8I9j0K1l2M3n4",
  "chain": "tron",
  "token": "USDT",
  "amount_usd": 49.99,
  "amount_atomic": "49990000",
  "status": "pending",
  "tx_hash": null,
  "confirmations": 0,
  "required_confirmations": 19,
  "expires_at": "2025-06-15T11:00:00Z",
  "detected_at": null,
  "confirmed_at": null,
  "created_at": "2025-06-15T10:30:00Z",
  "metadata": {
    "order_id": "ord_12345",
    "customer_email": "user@example.com"
  }
}
Idempotency — Always include an Idempotency-Key header (UUID format) when creating checkouts. If your request is retried (e.g., due to a network timeout), billing.io returns the original checkout instead of creating a duplicate. Keys expire after 24 hours. See the Idempotency guide for details.

Step 3: Redirect the Customer

After creating a checkout, direct your customer to pay. You have two options:

Option A: Hosted checkout page

Redirect the customer to the billing.io hosted checkout page:
https://pay.billing.io/checkout/{checkout_id}
This page shows the deposit address, QR code, amount, countdown timer, and live status updates.

Option B: Embed in your UI

Use the checkout data from the API response to build your own payment UI. Display:
  • The deposit_address for the customer to send crypto to
  • The amount_atomic (token amount in smallest unit) or amount_usd
  • A QR code encoding the deposit address
  • A countdown timer based on expires_at

Step 4: Monitor Payment Status

Once the customer sends crypto, the checkout moves through these statuses:
pending -> detected -> confirming -> confirmed
                                  \-> expired
                                  \-> failed
StatusDescription
pendingWaiting for payment. No transaction detected yet.
detectedTransaction seen on-chain but not yet confirmed.
confirmingBlock confirmations in progress.
confirmedPayment fully confirmed. This is a terminal state.
expiredCheckout TTL exceeded without payment. Terminal state.
failedUnrecoverable error. Terminal state.
Webhooks are the recommended integration. Polling is provided as a fallback or for debugging.

Option A: Poll for status

Use the lightweight status endpoint for efficient polling. It returns a polling_interval_ms hint.
curl https://api.billing.io/v1/checkouts/co_9a8b7c6d5e4f/status \
  -H "Authorization: Bearer sk_live_xxx"
The status response looks like:
{
  "checkout_id": "co_9a8b7c6d5e4f",
  "status": "confirming",
  "tx_hash": "abc123def456...",
  "confirmations": 12,
  "required_confirmations": 19,
  "detected_at": "2025-06-15T10:35:00Z",
  "confirmed_at": null,
  "polling_interval_ms": 2000
}
Webhooks push events to your server in real time, so you do not need to poll. See the Webhooks guide for full setup instructions.

Step 5: Handle the checkout.completed Event

When a checkout is fully confirmed on-chain, billing.io fires a checkout.completed webhook event. This is the signal to fulfill the order.

Full webhook payload

{
  "event_id": "evt_7f8a9b0c1d2e3f4a",
  "type": "checkout.completed",
  "checkout_id": "co_9a8b7c6d5e4f",
  "data": {
    "checkout_id": "co_9a8b7c6d5e4f",
    "deposit_address": "TXrkA1b2C3d4E5f6G7h8I9j0K1l2M3n4",
    "chain": "tron",
    "token": "USDT",
    "amount_usd": 49.99,
    "amount_atomic": "49990000",
    "status": "confirmed",
    "tx_hash": "abc123def456789abc123def456789abc123def456789abc123def456789abcd",
    "confirmations": 19,
    "required_confirmations": 19,
    "expires_at": "2025-06-15T11:00:00Z",
    "detected_at": "2025-06-15T10:35:00Z",
    "confirmed_at": "2025-06-15T10:42:00Z",
    "created_at": "2025-06-15T10:30:00Z",
    "metadata": {
      "order_id": "ord_12345",
      "customer_email": "user@example.com"
    }
  },
  "created_at": "2025-06-15T10:42:00Z"
}

Handling the event in your server

const crypto = require("crypto");

app.post("/webhooks/billing", express.raw({ type: "application/json" }), (req, res) => {
  // 1. Verify the signature (see Webhooks guide for details)
  const signature = req.headers["x-billing-signature"];
  // ... verification logic ...

  // 2. Parse the event
  const event = JSON.parse(req.body);

  // 3. Handle the event
  switch (event.type) {
    case "checkout.completed":
      const checkout = event.data;
      const orderId = checkout.metadata.order_id;

      // Fulfill the order in your system
      await fulfillOrder(orderId, {
        tx_hash: checkout.tx_hash,
        amount_usd: checkout.amount_usd,
        chain: checkout.chain,
        token: checkout.token,
        confirmed_at: checkout.confirmed_at,
      });
      break;

    case "checkout.expired":
      // Handle expired checkout (e.g., release inventory)
      await handleExpiredCheckout(event.data.metadata.order_id);
      break;

    case "checkout.failed":
      // Handle failure
      await handleFailedCheckout(event.data.metadata.order_id);
      break;
  }

  // 4. Respond with 200 immediately
  res.status(200).json({ received: true });
});
Always verify webhook signatures before processing events. An attacker could send fake events to your endpoint. See the Webhooks guide for signature verification code in Node.js, Python, and Go.

Complete Flow Summary

┌─────────────┐     POST /payment-methods     ┌─────────────┐
│  Your Server │ ───────────────────────────── │ billing.io  │
│              │                               │   API       │
│              │     POST /checkouts           │             │
│              │ ───────────────────────────── │             │
│              │ <── checkout_id + deposit_addr│             │
│              │                               │             │
│              │     Redirect customer ──────> │  Checkout   │
│              │                               │  Page       │
│              │                               │             │
│              │                               │ Chain       │
│              │ <── webhook: checkout.completed│ Watcher    │
│              │                               │ confirms tx │
│              │     Fulfill order             │             │
└─────────────┘                               └─────────────┘

Next Steps