Quickstart
Go from zero to a confirmed test payment in under 10 minutes.
Go from zero to a confirmed test payment in under 10 minutes. Bag 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 Bag account — sign up at 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
Install the SDK
Bag has a first-party TypeScript SDK:
npm install @tbagtapp/sdkNo Python SDK yet — use requests to hit the REST API directly. Same endpoints, same behavior:
pip install requestsCreate your first payment link
A payment link is a reusable checkout URL. You create one for each product or plan you sell, and Bag gives you a hosted checkout page your customers can pay through — no frontend work required.
import { Bag } from "@tbagtapp/sdk";
// Initialize the client with your test API key
const bag = new Bag({
apiKey: process.env.BAG_API_KEY!,
});
async function main() {
// Create a payment link for a $29.99 product on Base Sepolia (testnet)
const link = await bag.paymentLinks.create({
name: "Pro Plan", // Display name shown on the checkout page
amount: 29.99, // Price in USD
network: "base_sepolia", // Testnet network — no real money moves
});
console.log("Payment link created:");
console.log(` ID: ${link.id}`);
console.log(` Name: ${link.name}`);
console.log(` Amount: $${link.amount} ${link.currency}`);
console.log(` Token: ${link.token}`);
console.log(` Active: ${link.active}`);
console.log(` URL: https://getbags.app/pay/${link.id}`);
}
main().catch(console.error);Run it:
npx tsx create-link.tsimport os
import requests
API_KEY = os.environ["BAG_API_KEY"]
BASE_URL = "https://getbags.app"
# Create a payment link for a $29.99 product on Base Sepolia (testnet)
response = requests.post(
f"{BASE_URL}/api/payment-links",
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
},
json={
"name": "Pro Plan", # Display name shown on the checkout page
"amount": 29.99, # Price in USD
"network": "base_sepolia", # Testnet network — no real money moves
},
)
result = response.json()
if result["status"] == "success":
link = result["data"]
print("Payment link created:")
print(f" ID: {link['id']}")
print(f" Name: {link['name']}")
print(f" Amount: ${link['amount']} {link['currency']}")
print(f" Token: {link['token']}")
print(f" Active: {link['active']}")
print(f" URL: https://getbags.app/pay/{link['id']}")
else:
print(f"Error: {result.get('message', 'Unknown error')}")Run it:
python create_link.pyWhat you get back
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Pro Plan",
"amount": 29.99,
"currency": "USD",
"network": "base_sepolia",
"token": "USDC",
"active": true,
"livemode": false,
"merchantWalletAddress": "0xYourWalletAddress",
"totalCollected": 0,
"totalTransactions": 0,
"createdAt": "2026-03-01T12:00:00.000Z",
"updatedAt": "2026-03-01T12:00:00.000Z"
}| Field | What it means |
|---|---|
id | Unique identifier. Your checkout URL is https://getbags.app/pay/{id} |
amount | Price in USD |
currency | Always USD (default) |
network | The blockchain network this link accepts payments on |
token | The token customers pay with — defaults to USDC |
active | Whether the link accepts new payments |
livemode | false for test keys, true for live keys |
merchantWalletAddress | The wallet that receives funds |
totalCollected / totalTransactions | Running totals — both start at 0 |
Save the id. Your checkout page is live at https://getbags.app/pay/{id}. Share this URL with customers, embed it in your app, or redirect to it from a "Buy" button.
Handle the webhook
When a customer pays, the transaction confirms on-chain asynchronously. Bag 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 |
|---|---|---|
payment.completed | Payment confirmed on-chain | Fulfill the order, grant access, send a receipt |
payment.failed | Transaction reverted or timed out | Notify the customer, allow retry |
subscription.created | Recurring payment subscription created | Store subscription ID, set up recurring access |
subscription.updated | Subscription plan or details changed | Update stored plan info |
subscription.renewed | Recurring payment succeeded | Extend access period |
subscription.renewal_due | Upcoming renewal requires action | Notify customer, ensure funds available |
subscription.past_due | Renewal payment failed, grace period active | Warn customer, retry or restrict access |
subscription.canceled | Subscription permanently canceled | Revoke access at period end |
Setup:
- Go to the Bag 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 in your browser:
https://getbags.app/pay/YOUR_PAYMENT_LINK_ID - Connect a wallet (MetaMask, Coinbase Wallet, or Phantom for Solana) on the correct testnet
- Approve the USDC transfer — the 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
payment.completedevent logged - Check the dashboard — the transaction appears under your payment link with full details
Expected webhook output
When the payment confirms, your webhook server logs something like:
Received event: payment.completed
Timestamp: 2026-03-01T12:05:00.000Z
Payment completed: 29.99 USDC on base_sepolia
Session: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Tx Hash: 0xabc123def456789...Verification checklist
- Payment link created successfully
- Webhook server running and reachable (via ngrok or deployed)
- Checkout page loads at
https://getbags.app/pay/YOUR_LINK_ID - Payment completes on testnet
- Webhook received with valid signature
- Transaction visible in dashboard
What's next
Build a Custom Checkout
Control the payment flow from your own UI with the Checkout Sessions API.
Handle Webhooks (Deep Dive)
Retry behavior, best practices, and advanced webhook patterns.
Accept Payments on Multiple Chains
Let customers pay on Base, Ethereum, Polygon, or Solana.
Calculate Tax Before Checkout
Request tax quotes ahead of time for custom checkout flows.
Request a Payout
Payout currency options, fees, and settlement cycles.
Go Live Checklist
Complete KYB, generate a live key, and switch to production.
API Reference
Full endpoint documentation for every Bag API.
Quick reference
| Resource | URL |
|---|---|
| Dashboard | getbags.app |
| SDK (npm) | npm install @tbagtapp/sdk |
| API Base URL | https://getbags.app |
| OpenAPI Spec | getbags.app/openapi.yaml |