Quickstart
Go from zero to a confirmed test payment in under 10 minutes.
Bags is a Merchant of Record — we handle tax, compliance, invoicing, and settlement so you can focus on your product.
Before you start
You'll need three things:
- Node.js 18+ (for TypeScript examples) or Python 3.8+ (for Python examples)
- A Bags account — sign up at www.getbags.app (takes under 60 seconds)
- A test API key — after signing up, go to Developer Settings in the dashboard and generate a test key
Your test key looks like this:
bag_test_sk_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6- Test keys (
bag_test_sk_*) hit the sandbox. No real money moves. Use these while building. - Live keys (
bag_live_sk_*) hit production. You'll need to complete KYB verification first. - Your full key is shown once at creation. Copy it and store it somewhere safe.
Set it as an environment variable so the examples below work as-is:
export BAG_API_KEY="bag_test_sk_your_key_here"Never expose your API key in client-side code, public repos, or browser bundles. Treat it like a password. Use environment variables or a secrets manager.
The quickstart
Set up your environment
Bags is a plain REST API — any HTTP client works. Pick the native client for your language:
Node 18+ ships with a global fetch, so there's nothing to install:
node --version # should be v18.0.0 or higherUse requests to hit the REST API directly:
pip install requestsCreate a product and checkout (golden path)
The primary integration path is V1 checkouts: create a catalog product once, then create a hosted checkout session per purchase.
POST /api/v1/products— define what you're sellingPOST /api/v1/checkouts— get a one-time hosted URL (data.url) and sessionidfor webhooks- Redirect your customer to
data.url
Sessions expire 30 minutes after creation if unpaid.
const API_KEY = process.env.BAG_API_KEY!;
const BASE_URL = "https://www.getbags.app";
async function main() {
const productRes = await fetch(`${BASE_URL}/api/v1/products`, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
name: "Pro Plan",
amount: 29.99,
network: "base_sepolia",
}),
});
const product = await productRes.json();
if (product.status !== "success") throw new Error(product.message);
const checkoutRes = await fetch(`${BASE_URL}/api/v1/checkouts`, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
productId: product.data.id,
network: "base_sepolia",
externalCustomerId: "cus_123",
metadata: { orderId: "ord_456" },
}),
});
const checkout = await checkoutRes.json();
if (checkout.status !== "success") throw new Error(checkout.message);
console.log("Checkout ready:");
console.log(` Session: ${checkout.data.id}`);
console.log(` URL: ${checkout.data.url}`);
}
main().catch(console.error);import os
import uuid
import requests
API_KEY = os.environ["BAG_API_KEY"]
BASE_URL = "https://www.getbags.app"
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
}
product = requests.post(
f"{BASE_URL}/api/v1/products",
headers={**headers, "Idempotency-Key": str(uuid.uuid4())},
json={"name": "Pro Plan", "amount": 29.99, "network": "base_sepolia"},
).json()
if product["status"] != "success":
raise RuntimeError(product.get("message"))
checkout = requests.post(
f"{BASE_URL}/api/v1/checkouts",
headers={**headers, "Idempotency-Key": str(uuid.uuid4())},
json={
"productId": product["data"]["id"],
"network": "base_sepolia",
"externalCustomerId": "cus_123",
"metadata": {"orderId": "ord_456"},
},
).json()
if checkout["status"] != "success":
raise RuntimeError(checkout.get("message"))
print("Checkout ready:")
print(f" Session: {checkout['data']['id']}")
print(f" URL: {checkout['data']['url']}")API reference:
POST /api/v1/products ·
POST /api/v1/checkouts ·
GET /api/v1/checkouts/{id}.
Optional: payment links. If you prefer a reusable URL (/pay/{linkId}) shared across many customers, use POST /api/payment-links instead of per-session checkouts. See Create a Payment Link.
Endpoint cheat sheet: POST /api/v1/checkouts = merchant hosted session (this quickstart). POST /api/payment-links = reusable link. POST /api/checkout/session = buyer wallet step in custom checkout. See Choose Your Integration.
Handle the webhook
When a customer pays, the transaction confirms on-chain asynchronously. Bags sends a POST request to your server when it's done. Webhooks are the source of truth for payment status — never rely on client-side confirmation.
| Event | When it fires | What to do |
|---|---|---|
checkout.completed | Hosted checkout paid and confirmed on-chain | Fulfill the order, grant access, send a receipt |
checkout.failed | Transaction reverted or verification failed | Notify the customer, surface the reason |
checkout.cancelled | Customer cancelled before broadcast | Release holds, mark order cancelled |
checkout.expired | Checkout not paid within 30 minutes | Release holds, optionally create a new checkout |
payment.refunded | Refund processed against a completed transaction | Reverse fulfillment, credit the customer |
Use checkout.* events. They cover the full payment lifecycle and are keyed on sessionId, matching the id from POST /api/v1/checkouts. Subscription events are coming soon — see Webhook Events.
Setup:
- Go to the Bags dashboard → Developer Settings → Webhooks
- Enter your server URL (e.g.
https://yourapp.com/webhooks/bag) - Copy the webhook secret (
whsec_*) and store it as an environment variable
export BAG_WEBHOOK_SECRET="whsec_your_secret_here"For local development, use ngrok to expose your localhost:
ngrok http 4000See Handle Webhooks Securely for full implementation with signature verification, payload examples, retry behavior, and best practices.
Test it end-to-end
Everything you've built so far uses test mode. Here's how to run a full payment without spending real money.
Get testnet tokens
You need testnet USDC and native tokens for gas. See Environments for faucet links and network details.
Walk through a payment
- Open the checkout URL from step 2 (
data.url) in your browser - Connect a wallet (MetaMask, Coinbase Wallet, or Phantom for Solana) on the correct testnet
- Approve the USDC transfer — the hosted checkout UI walks you through it
- Wait for on-chain confirmation — usually a few seconds on testnets
- Check your webhook server — you should see a
checkout.completedevent logged - Check the dashboard — the transaction appears with full details
Expected webhook output
When the payment confirms, your webhook server logs something like:
Received event: checkout.completed
Timestamp: 2026-03-01T12:05:00.000Z
Checkout completed: 29.99 USDC on base_sepolia
Session: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Tx Hash: 0xabc123def456789...Verification checklist
- Product and checkout created successfully
- Webhook server running and reachable (via ngrok or deployed)
- Hosted checkout page loads at
data.url - Payment completes on testnet
- Webhook received with valid signature
- Transaction visible in dashboard
What's next
Sample Integration
Complete working example with checkout creation and webhook handling.
Handle Webhooks
Signature verification, retry behavior, and best practices.
Accept Payments on Multiple Chains
Let customers pay on Base, Polygon, or Solana.
Go Live Checklist
Complete KYB, generate a live key, and switch to production.
API Reference
Full endpoint documentation for every Bags API.
Quick reference
| Resource | URL |
|---|---|
| Dashboard | getbags.app |
| API Base URL | https://www.getbags.app |
| OpenAPI Spec | getbags.app/openapi.yaml |