Skip to main content
The billing.io sandbox is a full-featured testing environment that mirrors production. Create checkouts, simulate payments, test subscriptions, and validate webhook handlers — all without real crypto. Sandbox and live data are fully isolated. Sandbox resources cannot interact with live resources.

Getting Sandbox API Keys

Sandbox API keys have the prefix sk_test_. You can find them in your dashboard at Developers > API Keys.
sk_test_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
Use this key in the Authorization header for all sandbox requests:
curl https://api.billing.io/v1/checkouts \
  -H "Authorization: Bearer sk_test_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
Sandbox keys are available on all plans, including the Free plan. You do not need a paid plan to test your integration.

Sandbox vs Live Environment

AspectSandbox (sk_test_)Live (sk_live_)
Real cryptoNoYes
On-chain transactionsSimulatedReal blockchain verification
Webhook deliveriesFull delivery with signaturesFull delivery with signatures
API rate limitsSame as liveBased on your plan
Data isolationCompletely separateCompletely separate
Payment simulationPOST /sandbox/simulate-paymentReal on-chain payments
HTTP webhooksAllowed (for local dev)HTTPS only
Chain-watcherSimulated confirmationsReal block confirmations
Sandbox and live data are fully isolated. Customers, checkouts, subscriptions, and all other resources created with sk_test_ keys exist only in the sandbox environment. They will not appear when using sk_live_ keys.

Creating Test Checkouts

Create checkouts in sandbox exactly as you would in production:
curl -X POST https://api.billing.io/v1/checkouts \
  -H "Authorization: Bearer sk_test_xxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
  -d '{
    "amount_usd": 25.00,
    "chain": "tron",
    "token": "USDT",
    "metadata": {
      "order_id": "test_ord_001",
      "test": "true"
    }
  }'
The checkout is created with status: "pending" and a test deposit address. In sandbox, no real crypto needs to be sent — use the simulation endpoint instead.

Simulating Payments

The sandbox provides a dedicated endpoint to simulate payment confirmation for a checkout. This triggers the same flow as a real on-chain payment: the checkout moves through detected -> confirming -> confirmed, and all associated webhook events fire.
curl -X POST https://api.billing.io/v1/sandbox/simulate-payment \
  -H "Authorization: Bearer sk_test_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "checkout_id": "co_9a8b7c6d5e4f"
  }'
The POST /sandbox/simulate-payment endpoint is only available with sandbox API keys (sk_test_). Calling it with a live key returns a 400 error.

What happens when you simulate a payment

  1. The checkout status transitions: pending -> detected -> confirming -> confirmed
  2. A simulated tx_hash is generated
  3. Webhook events fire in order: checkout.payment_detected, checkout.confirming, checkout.completed
  4. If the checkout is linked to a subscription renewal, the renewal is marked as paid and the subscription period advances
  5. A revenue event is created

Testing Webhook Deliveries

Set up a local webhook receiver

For local development, use a tunnel service to expose your local server:
# Start your local server
npm run dev  # Runs on port 3000

# In another terminal, start ngrok
ngrok http 3000
ngrok gives you a public URL like https://abc123.ngrok.io. Register this as your webhook endpoint.

Register a sandbox webhook endpoint

curl -X POST https://api.billing.io/v1/webhooks \
  -H "Authorization: Bearer sk_test_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://abc123.ngrok.io/webhooks/billing",
    "events": [
      "checkout.created",
      "checkout.payment_detected",
      "checkout.confirming",
      "checkout.completed",
      "checkout.expired"
    ],
    "description": "Local development testing"
  }'
Sandbox webhooks allow HTTP URLs for local development convenience. In production, only HTTPS endpoints are accepted.

Trigger webhook events

# 1. Create a test checkout
CHECKOUT=$(curl -s -X POST https://api.billing.io/v1/checkouts \
  -H "Authorization: Bearer sk_test_xxx" \
  -H "Content-Type: application/json" \
  -d '{"amount_usd": 10.00, "chain": "tron", "token": "USDT"}')

CHECKOUT_ID=$(echo $CHECKOUT | jq -r '.checkout_id')
echo "Created checkout: $CHECKOUT_ID"

# 2. Simulate payment -- this fires checkout.completed webhook
curl -X POST https://api.billing.io/v1/sandbox/simulate-payment \
  -H "Authorization: Bearer sk_test_xxx" \
  -H "Content-Type: application/json" \
  -d "{\"checkout_id\": \"$CHECKOUT_ID\"}"
Your local webhook handler should receive the events. Verify that:
  • The X-Billing-Signature header is present
  • Your signature verification code validates correctly
  • Your handler processes the event and responds with 200

Testing Subscriptions in Sandbox

Test the full subscription lifecycle in sandbox:

1. Create test resources

# Create a test customer
curl -X POST https://api.billing.io/v1/customers \
  -H "Authorization: Bearer sk_test_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "test@example.com",
    "name": "Test Customer"
  }'

# Create a test plan
curl -X POST https://api.billing.io/v1/subscriptions/plans \
  -H "Authorization: Bearer sk_test_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Test Pro Plan",
    "amount_usd": 9.99,
    "interval": "monthly",
    "trial_days": 0,
    "token": "USDT",
    "chain": "tron"
  }'

# Create a subscription
curl -X POST https://api.billing.io/v1/subscriptions \
  -H "Authorization: Bearer sk_test_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "customer_id": "cus_...",
    "plan_id": "plan_..."
  }'

2. Test renewal payments

When a subscription renewal creates a checkout, simulate the payment:
# List renewals to find the checkout
curl "https://api.billing.io/v1/subscriptions/renewals?subscription_id=sub_..." \
  -H "Authorization: Bearer sk_test_xxx"

# Simulate the renewal payment
curl -X POST https://api.billing.io/v1/sandbox/simulate-payment \
  -H "Authorization: Bearer sk_test_xxx" \
  -H "Content-Type: application/json" \
  -d '{"checkout_id": "co_..."}'

3. Test entitlement checks

curl "https://api.billing.io/v1/subscriptions/entitlements/check?customer_id=cus_...&feature_key=api_requests" \
  -H "Authorization: Bearer sk_test_xxx"

4. Test lifecycle transitions

# Pause
curl -X PATCH "https://api.billing.io/v1/subscriptions/sub_..." \
  -H "Authorization: Bearer sk_test_xxx" \
  -H "Content-Type: application/json" \
  -d '{"action": "pause"}'

# Resume
curl -X PATCH "https://api.billing.io/v1/subscriptions/sub_..." \
  -H "Authorization: Bearer sk_test_xxx" \
  -H "Content-Type: application/json" \
  -d '{"action": "resume"}'

# Cancel at period end
curl -X PATCH "https://api.billing.io/v1/subscriptions/sub_..." \
  -H "Authorization: Bearer sk_test_xxx" \
  -H "Content-Type: application/json" \
  -d '{"action": "cancel"}'

Testing Payouts in Sandbox

Test the payout orchestration flow without sending real crypto:
# 1. Create a payout intent
curl -X POST https://api.billing.io/v1/payouts \
  -H "Authorization: Bearer sk_test_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "recipient_address": "TXrkA1b2C3d4E5f6G7h8I9j0K1l2M3n4",
    "chain": "tron",
    "token": "USDT",
    "amount": 100.00,
    "currency": "USD",
    "description": "Test payout"
  }'

# 2. Approve
curl -X PATCH "https://api.billing.io/v1/payouts/po_..." \
  -H "Authorization: Bearer sk_test_xxx" \
  -H "Content-Type: application/json" \
  -d '{"status": "approved"}'

# 3. Submit a test tx hash (any string works in sandbox)
curl -X POST "https://api.billing.io/v1/payouts/po_.../execute" \
  -H "Authorization: Bearer sk_test_xxx" \
  -H "Content-Type: application/json" \
  -d '{"tx_hash": "test_tx_abc123def456"}'

# 4. Check settlements
curl "https://api.billing.io/v1/payouts/settlements?payout_intent_id=po_..." \
  -H "Authorization: Bearer sk_test_xxx"

# 5. Check reconciliation
curl "https://api.billing.io/v1/payouts/reconciliation" \
  -H "Authorization: Bearer sk_test_xxx"

End-to-End Test Script

Here is a complete Node.js script that tests the full checkout flow:
const API_BASE = "https://api.billing.io/v1";
const API_KEY = "sk_test_your_key_here";

async function api(method, path, body = null) {
  const options = {
    method,
    headers: {
      "Authorization": `Bearer ${API_KEY}`,
      "Content-Type": "application/json",
    },
  };
  if (body) options.body = JSON.stringify(body);

  const response = await fetch(`${API_BASE}${path}`, options);
  return response.json();
}

async function runE2ETest() {
  console.log("=== billing.io Sandbox E2E Test ===\n");

  // 1. Create a checkout
  console.log("1. Creating checkout...");
  const checkout = await api("POST", "/checkouts", {
    amount_usd: 42.00,
    chain: "tron",
    token: "USDT",
    metadata: { test_run: "e2e_001" },
  });
  console.log(`   Checkout created: ${checkout.checkout_id} (${checkout.status})\n`);

  // 2. Check initial status
  console.log("2. Checking status...");
  const status1 = await api("GET", `/checkouts/${checkout.checkout_id}/status`);
  console.log(`   Status: ${status1.status}\n`);

  // 3. Simulate payment
  console.log("3. Simulating payment...");
  await api("POST", "/sandbox/simulate-payment", {
    checkout_id: checkout.checkout_id,
  });
  console.log("   Payment simulated.\n");

  // 4. Verify final status
  console.log("4. Verifying final status...");
  const status2 = await api("GET", `/checkouts/${checkout.checkout_id}/status`);
  console.log(`   Status: ${status2.status}`);
  console.log(`   TX Hash: ${status2.tx_hash}`);
  console.log(`   Confirmations: ${status2.confirmations}/${status2.required_confirmations}`);
  console.log(`   Confirmed at: ${status2.confirmed_at}\n`);

  // 5. Verify via full checkout object
  console.log("5. Fetching full checkout...");
  const finalCheckout = await api("GET", `/checkouts/${checkout.checkout_id}`);
  console.log(`   Final status: ${finalCheckout.status}`);
  console.log(`   Amount: $${finalCheckout.amount_usd} (${finalCheckout.amount_atomic} atomic)\n`);

  console.log("=== Test complete ===");
}

runE2ETest().catch(console.error);

Moving to Production Checklist

Before switching from sandbox to live, verify the following:
1

Upgrade to a paid plan

Sandbox is free, but live payments are available on Starter plans and above. Upgrade at Account > Plans & Usage in the dashboard.
2

Switch to live API keys

Replace sk_test_ keys with sk_live_ keys in your environment variables. Never commit API keys to source control.
3

Configure live payment methods

Create payment methods with your real wallet addresses. Ensure you control the private keys for these wallets.
4

Register live webhook endpoints

Create new webhook endpoints for your production URLs. Ensure they use HTTPS. Store the webhook signing secret securely.
5

Verify signature verification is working

Confirm your webhook handler correctly verifies X-Billing-Signature headers and rejects invalid signatures.
6

Implement idempotency

Ensure all POST /checkouts and other create requests include an Idempotency-Key header. See the Idempotency guide.
7

Handle all terminal states

Your integration should handle confirmed, expired, and failed checkout statuses. Test each path in sandbox before going live.
8

Set up monitoring

Monitor your webhook endpoint uptime, API error rates, and checkout conversion rates. Check the Developers > Event Log page regularly.
9

Remove sandbox simulation calls

Ensure no code paths call POST /sandbox/simulate-payment with live keys. This endpoint rejects live API keys.

Next Steps