Skip to main content

Prerequisites

Before you start, you need:
  1. A billing.io account — sign up at app.billing.io
  2. At least one payment method configured (chain + token + wallet address)
  3. Your API key from the dashboard (prefixed sk_test_ for sandbox or sk_live_ for production)
Use your sk_test_ key to follow this guide in sandbox mode. You can simulate payments without real crypto.

1

Get your API keys

Navigate to the Dashboard and copy your secret API key. Sandbox keys are prefixed with sk_test_ and production keys with sk_live_.
# Store your key as an environment variable
export BILLING_API_KEY="sk_test_your_key_here"
Never expose your secret API key in client-side code or commit it to version control. Use environment variables or a secrets manager.
2

Create your first checkout

Call the checkout endpoint to generate a deposit address. The customer will send crypto to this address.
curl -X POST https://api.billing.io/v1/checkouts \
  -H "Authorization: Bearer $BILLING_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "amount_usd": 49.99,
    "chain": "tron",
    "token": "USDT",
    "metadata": {
      "order_id": "ord_12345",
      "customer_email": "customer@example.com"
    }
  }'
The response includes the deposit address and checkout details:
{
  "checkout_id": "co_a1b2c3d4e5f6a7b8c9d0e1f2",
  "deposit_address": "TXqH4jB8nPz1rL9kVw2mYd6sQ5cFgA3hJe",
  "chain": "tron",
  "token": "USDT",
  "amount_usd": 49.99,
  "amount_atomic": "49990000",
  "status": "pending",
  "confirmations": 0,
  "required_confirmations": 19,
  "expires_at": "2025-01-15T12:30:00Z",
  "created_at": "2025-01-15T12:00:00Z",
  "metadata": {
    "order_id": "ord_12345",
    "customer_email": "customer@example.com"
  }
}
Use the Idempotency-Key header to safely retry requests. If a request with the same key was already processed, the original response is returned. Keys expire after 24 hours.
3

Handle the webhook

When the payment is confirmed on-chain, billing.io sends a checkout.completed event to your registered webhook endpoint.First, register a webhook endpoint:
curl -X POST https://api.billing.io/v1/webhooks \
  -H "Authorization: Bearer $BILLING_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/webhooks/billing",
    "events": ["checkout.completed", "checkout.expired"]
  }'
The webhook signing secret (whsec_ prefix) is only returned once at creation time. Store it securely — you will need it to verify incoming webhook signatures.
Then handle incoming webhooks in your application:
Node.js
import crypto from "crypto";

export async function POST(request) {
  const body = await request.text();
  const signature = request.headers.get("X-Billing-Signature");

  // Parse the signature header
  const parts = Object.fromEntries(
    signature.split(",").map((p) => p.split("="))
  );
  const timestamp = parts.t;
  const receivedHmac = parts.v1;

  // Compute expected HMAC
  const payload = `${timestamp}.${body}`;
  const expectedHmac = crypto
    .createHmac("sha256", process.env.WEBHOOK_SECRET)
    .update(payload)
    .digest("hex");

  if (receivedHmac !== expectedHmac) {
    return new Response("Invalid signature", { status: 401 });
  }

  // Process the event
  const event = JSON.parse(body);

  switch (event.type) {
    case "checkout.completed":
      const checkout = event.data;
      console.log(`Payment confirmed: ${checkout.checkout_id}`);
      console.log(`Amount: $${checkout.amount_usd}`);
      console.log(`Tx: ${checkout.tx_hash}`);
      // Fulfill the order in your system
      break;

    case "checkout.expired":
      console.log(`Checkout expired: ${event.data.checkout_id}`);
      // Mark the order as expired
      break;
  }

  return new Response("OK", { status: 200 });
}
A checkout.completed webhook payload looks like this:
{
  "event_id": "evt_f1e2d3c4b5a6f7e8d9c0b1a2",
  "type": "checkout.completed",
  "checkout_id": "co_a1b2c3d4e5f6a7b8c9d0e1f2",
  "data": {
    "checkout_id": "co_a1b2c3d4e5f6a7b8c9d0e1f2",
    "deposit_address": "TXqH4jB8nPz1rL9kVw2mYd6sQ5cFgA3hJe",
    "chain": "tron",
    "token": "USDT",
    "amount_usd": 49.99,
    "amount_atomic": "49990000",
    "status": "confirmed",
    "tx_hash": "a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd",
    "confirmations": 19,
    "required_confirmations": 19,
    "confirmed_at": "2025-01-15T12:05:30Z",
    "metadata": {
      "order_id": "ord_12345",
      "customer_email": "customer@example.com"
    }
  },
  "created_at": "2025-01-15T12:05:31Z"
}

Polling as a fallback

If you prefer polling over webhooks (or want a fallback), use the lightweight status endpoint:
curl https://api.billing.io/v1/checkouts/co_a1b2c3d4e5f6a7b8c9d0e1f2/status \
  -H "Authorization: Bearer $BILLING_API_KEY"
The response includes a polling_interval_ms hint telling you how often to poll:
{
  "checkout_id": "co_a1b2c3d4e5f6a7b8c9d0e1f2",
  "status": "confirming",
  "tx_hash": "a1b2c3...",
  "confirmations": 12,
  "required_confirmations": 19,
  "polling_interval_ms": 2000
}

Test in sandbox

Use your sk_test_ API key and the sandbox environment to test the full flow without real crypto. You can simulate payment confirmations from the Sandbox page in the dashboard.

Sandbox Testing Guide

Learn how to simulate payments, test webhooks, and validate your integration in sandbox mode.

What’s next?

Accept Payments

Full guide to payment methods, checkouts, and payment links.

Subscription Billing

Set up recurring billing with plans, trials, and renewals.

Webhooks

Deep dive into webhook setup, signature verification, and retry behavior.

Idempotency

Safely retry requests and prevent duplicate operations.