Build a Custom Checkout
Use the Checkout Sessions API to control the payment flow from your own UI.
Hosted checkout (/pay)
If you do not need a custom UI, use V1 checkouts or payment links. Bags hosts the page at data.url or /pay/{id} with wallet connect, tax display, and expiry handling built in.
Payment links give you a hosted checkout page with zero frontend work. But if you need more control — embedding checkout in your app, collecting customer details upfront, or building a custom payment UI — use the Checkout Sessions API.
A checkout session tracks a single payment attempt from tax calculation through on-chain confirmation.
Buyer session API — not merchant catalog. POST /api/checkout/session is for one payer attempt inside your checkout UI. Required body fields include linkId (from an existing payment link), quoteToken (from POST /api/tax/quote), walletAddress, network, and walletType.
Merchants creating shareable products or hosted flows should use:
POST /api/payment-links— reusable link (/pay/{id}) for many customers.POST /api/v1/checkouts— server-side hosted session per purchase (productId→ sessionurl).
The checkout flow
Every custom checkout follows four steps:
- Get a tax quote — Calculate tax based on the customer's address
- Create a session — Lock in the price and get a session ID
- Submit the transaction — Customer pays on-chain, you submit the tx hash
- Finalize — Poll until on-chain confirmation, Bags fires the webhook
Your Server Bags API Blockchain
| | |
|-- POST /api/tax/quote ----->| |
|<-- quoteToken, totals ------| |
| | |
|-- POST /api/checkout/session -->| |
|<-- sessionId, expiresAt --------| |
| | |
| (customer signs tx) | |
| | tx confirmed
|-- POST /api/checkout/submit --->| |
|<-- status: txn_broadcast -------| |
| | |
|-- POST /api/checkout/finalize ->|--- verify on-chain --->|
|<-- status: complete ------------| |
| | |
| (webhook: checkout.completed) | |Step 1: Get a tax quote
Before creating a session, request a tax quote. Bags routes calculation to regional providers based on the customer's country (see Tax Quotes).
const response = await fetch("https://www.getbags.app/api/tax/quote", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.BAG_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
paymentLinkId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
customerAddress: {
address_line_1: "100 Main St",
address_city: "San Francisco",
address_province: "CA",
address_postal_code: "94105",
address_country: "US",
address_type: "billing",
},
}),
});
const { data: quote } = await response.json();
console.log(`Subtotal: $${(quote.subtotalCents / 100).toFixed(2)}`);
console.log(`Tax: $${(quote.taxCents / 100).toFixed(2)}`);
console.log(`Total: $${(quote.totalCents / 100).toFixed(2)}`);import os
import requests
API_KEY = os.environ["BAG_API_KEY"]
response = requests.post(
"https://www.getbags.app/api/tax/quote",
headers={"Content-Type": "application/json"},
json={
"paymentLinkId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"customerAddress": {
"address_line_1": "100 Main St",
"address_city": "San Francisco",
"address_province": "CA",
"address_postal_code": "94105",
"address_country": "US",
"address_type": "billing",
},
},
)
quote = response.json()["data"]
print(f"Subtotal: ${quote['subtotalCents'] / 100:.2f}")
print(f"Tax: ${quote['taxCents'] / 100:.2f}")
print(f"Total: ${quote['totalCents'] / 100:.2f}")Tax quote response
{
"status": "success",
"data": {
"subtotalCents": 2999,
"taxCents": 262,
"totalCents": 3261,
"calculationId": "calc_abc123",
"quoteToken": "eyJhbGciOiJIUzI1NiJ9..."
}
}Save the quoteToken — you'll need it to create the session. The token is signed and expires, so create the session promptly.
Step 2: Create a checkout session
const sessionResponse = await fetch("https://www.getbags.app/api/checkout/session", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.BAG_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
linkId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
quoteToken: quote.quoteToken,
walletAddress: "0xCustomerWalletAddress",
walletType: "evm",
network: "base_sepolia",
customer: {
name: "Jane Doe",
email: "jane@example.com",
address: "100 Main St, San Francisco, CA 94105",
country: "US",
},
totalsSnapshot: {
subtotalCents: quote.subtotalCents,
taxCents: quote.taxCents,
totalCents: quote.totalCents,
calculationId: quote.calculationId,
},
}),
});
const { data: session } = await sessionResponse.json();
console.log(`Session ID: ${session.sessionId}`);
console.log(`Expires at: ${session.expiresAt}`);response = requests.post(
"https://www.getbags.app/api/checkout/session",
headers={"Content-Type": "application/json"},
json={
"linkId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"quoteToken": quote["quoteToken"],
"walletAddress": "0xCustomerWalletAddress",
"walletType": "evm",
"network": "base_sepolia",
"customer": {
"name": "Jane Doe",
"email": "jane@example.com",
"address": "100 Main St, San Francisco, CA 94105",
"country": "US",
},
"totalsSnapshot": {
"subtotalCents": quote["subtotalCents"],
"taxCents": quote["taxCents"],
"totalCents": quote["totalCents"],
"calculationId": quote["calculationId"],
},
},
)
session = response.json()["data"]
print(f"Session ID: {session['sessionId']}")
print(f"Expires at: {session['expiresAt']}")Session response
{
"status": "success",
"data": {
"sessionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "payment_session_created",
"quote": {
"subtotalCents": 2999,
"taxCents": 262,
"totalCents": 3261,
"calculationId": "calc_abc123"
},
"expiresAt": "2026-03-01T12:30:00.000Z"
}
}Sessions expire after 30 minutes. If the customer doesn't pay in time, create a new session.
Sessions are idempotent — if you create a session with the same payment link, wallet, network, and calculation ID, Bags returns the existing session instead of creating a duplicate.
Step 3: Submit the transaction hash
After the customer signs and broadcasts the on-chain transaction, submit the tx hash to Bags:
await fetch("https://www.getbags.app/api/checkout/submit", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.BAG_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
sessionId: session.sessionId,
txHash: "0xabc123def456...",
}),
});requests.post(
"https://www.getbags.app/api/checkout/submit",
headers={"Content-Type": "application/json"},
json={
"sessionId": session["sessionId"],
"txHash": "0xabc123def456...",
},
)Bags deduplicates by txHash — submitting the same hash twice is safe.
Step 4: Finalize
Poll the finalize endpoint until the transaction is confirmed on-chain:
let confirmed = false;
while (!confirmed) {
const result = await fetch("https://www.getbags.app/api/checkout/finalize", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sessionId: session.sessionId }),
}).then((r) => r.json());
if (result.data.confirmed) {
confirmed = true;
console.log("Payment confirmed!");
} else {
await new Promise((r) => setTimeout(r, 3000));
}
}import time
confirmed = False
while not confirmed:
result = requests.post(
"https://www.getbags.app/api/checkout/finalize",
headers={"Content-Type": "application/json"},
json={"sessionId": session["sessionId"]},
).json()
if result["data"].get("confirmed"):
confirmed = True
print("Payment confirmed!")
else:
time.sleep(3)When finalization succeeds, Bags:
- Marks the transaction as
completed - Records tax with the regional provider that issued the quote
- Fires a
checkout.completedwebhook (and a legacypayment.completed) - Sets the session status to
complete
Session statuses
| Status | Meaning |
|---|---|
payment_session_created | Session created, waiting for payment |
txn_broadcast | Transaction hash submitted |
txn_confirming | Waiting for on-chain confirmation |
txn_finalized | Confirmed on-chain |
complete | Payment fully processed |
failed | Transaction reverted or verification failed |
expired | Session timed out (30 min) |
manual_retry_needed | Requires manual intervention |
Check session status
You can poll a session's status at any time:
const response = await fetch(
"https://www.getbags.app/api/checkout/session?sessionId=a1b2c3d4-e5f6-7890-abcd-ef1234567890",
{
headers: { Authorization: `Bearer ${process.env.BAG_API_KEY}` },
},
);
const { data: status } = await response.json();
console.log(status.status);