Bag Docs
Getting Started

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. When your customer pays, they pay Bag — and Bag pays you. In between, we handle tax collection, compliance, invoicing, and cross-border settlement. You build your product. We handle the money stuff. Starting at 1.5% per transaction.

What Bag handles: Tax calculation, compliance, invoicing, payment processing, settlement.

What you handle: Your product, your UI, and a webhook endpoint to know when you get paid.


Before you start

You'll need three things:

  • Node.js 18+ (for TypeScript examples) or Python 3.8+ (for Python examples)
  • A Bag accountsign up at justusebag.xyz (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 @getbagsapp/sdk

No Python SDK yet — use requests to hit the REST API directly. Same endpoints, same behavior:

pip install requests

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 "@getbagsapp/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://justusebag.xyz/pay/${link._id}`);
}

main().catch(console.error);

Run it:

npx tsx create-link.ts
import os
import requests

API_KEY = os.environ["BAG_API_KEY"]
BASE_URL = "https://justusebag.xyz"

# 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://justusebag.xyz/pay/{link['_id']}")
else:
    print(f"Error: {result.get('message', 'Unknown error')}")

Run it:

python create_link.py

What you get back

{
  "_id": "665f1a2b3c4d5e6f7a8b9c0d",
  "name": "Pro Plan",
  "amount": 29.99,
  "currency": "USD",
  "network": "base_sepolia",
  "token": "USDC",
  "active": true,
  "merchantWalletAddress": "0xYourWalletAddress",
  "totalCollected": 0,
  "totalTransactions": 0,
  "createdAt": "2026-03-01T12:00:00.000Z",
  "updatedAt": "2026-03-01T12:00:00.000Z"
}
FieldWhat it means
_idUnique identifier. Your checkout URL is https://justusebag.xyz/pay/{_id}
amountPrice in USD
currencyAlways USD (default)
networkThe blockchain network this link accepts payments on
tokenThe token customers pay with — defaults to USDC
activeWhether the link accepts new payments
merchantWalletAddressThe wallet that receives funds
totalCollected / totalTransactionsRunning totals — both start at 0

Save the _id. Your checkout page is live at https://justusebag.xyz/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 to fulfill an order.

Events you need to handle

EventWhen it firesWhat to do
payment.completedPayment confirmed on-chainFulfill the order, grant access, send a receipt
payment.failedTransaction reverted or timed outNotify the customer, allow retry

Get your webhook secret

  1. Go to the Bag dashboardDeveloper SettingsWebhooks
  2. Enter your server URL (e.g. https://yourapp.com/webhooks/bag)
  3. Copy the webhook secret — it starts with whsec_ and is shown once
export BAG_WEBHOOK_SECRET="whsec_your_secret_here"

Local development

Your webhook endpoint needs to be reachable from the internet. For local development, use ngrok to tunnel traffic to your machine:

ngrok http 4000

Then register the ngrok URL (e.g. https://abc123.ngrok.io/webhooks/bag) as your webhook endpoint in the dashboard.

The webhook handler

Every webhook request includes two headers:

HeaderValue
X-Webhook-SignatureHMAC-SHA256 hex digest of the raw request body
X-Webhook-EventEvent name (e.g. payment.completed)

Always verify the signature before processing. If it doesn't match, the request didn't come from Bag — reject it immediately.

import express from "express";
import crypto from "crypto";

const app = express();
const WEBHOOK_SECRET = process.env.BAG_WEBHOOK_SECRET!;

app.post(
  "/webhooks/bag",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const signature = req.headers["x-webhook-signature"] as string;
    const event = req.headers["x-webhook-event"] as string;
    const rawBody = req.body as Buffer;

    // Verify the HMAC-SHA256 signature against your webhook secret
    const expected = crypto
      .createHmac("sha256", WEBHOOK_SECRET)
      .update(rawBody)
      .digest("hex");

    if (
      !signature ||
      !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
    ) {
      console.error("Invalid webhook signature — rejecting request");
      res.status(401).json({ error: "Invalid signature" });
      return;
    }

    const payload = JSON.parse(rawBody.toString());

    console.log(`Received event: ${event}`);
    console.log(`Timestamp: ${payload.timestamp}`);

    switch (event) {
      case "payment.completed": {
        const { sessionId, txHash, amount, network } = payload.data;
        console.log(`Payment completed: ${amount} USDC on ${network}`);
        console.log(`  Session: ${sessionId}`);
        console.log(`  Tx Hash: ${txHash}`);
        // TODO: fulfill the order — grant access, send receipt, etc.
        break;
      }
      case "payment.failed": {
        const { sessionId, reason } = payload.data;
        console.log(`Payment failed: ${reason}`);
        console.log(`  Session: ${sessionId}`);
        // TODO: notify the customer, allow retry
        break;
      }
      default:
        console.log(`Unhandled event: ${event}`);
    }

    res.status(200).json({ received: true });
  }
);

app.listen(4000, () => {
  console.log("Webhook server listening on http://localhost:4000");
});

Install dependencies and run:

npm install express
npx tsx webhook-server.ts
import os
import hmac
import hashlib
import json
from flask import Flask, request, jsonify

app = Flask(__name__)
WEBHOOK_SECRET = os.environ["BAG_WEBHOOK_SECRET"]


@app.route("/webhooks/bag", methods=["POST"])
def handle_webhook():
    signature = request.headers.get("X-Webhook-Signature", "")
    event = request.headers.get("X-Webhook-Event", "")
    raw_body = request.get_data()

    # Verify the HMAC-SHA256 signature against your webhook secret
    expected = hmac.new(
        WEBHOOK_SECRET.encode(),
        raw_body,
        hashlib.sha256,
    ).hexdigest()

    if not hmac.compare_digest(expected, signature):
        print("Invalid webhook signature — rejecting request")
        return jsonify({"error": "Invalid signature"}), 401

    payload = json.loads(raw_body)

    print(f"Received event: {event}")
    print(f"Timestamp: {payload['timestamp']}")

    if event == "payment.completed":
        data = payload["data"]
        print(f"Payment completed: {data['amount']} USDC on {data['network']}")
        print(f"  Session: {data['sessionId']}")
        print(f"  Tx Hash: {data['txHash']}")
        # TODO: fulfill the order — grant access, send receipt, etc.

    elif event == "payment.failed":
        data = payload["data"]
        print(f"Payment failed: {data['reason']}")
        print(f"  Session: {data['sessionId']}")
        # TODO: notify the customer, allow retry

    else:
        print(f"Unhandled event: {event}")

    return jsonify({"received": True}), 200


if __name__ == "__main__":
    app.run(port=4000)

Install dependencies and run:

pip install flask
python webhook_server.py

Webhook payload examples

payment.completed:

{
  "event": "payment.completed",
  "data": {
    "sessionId": "665f1a2b3c4d5e6f7a8b9c0d",
    "txHash": "0xabc123def456789...",
    "merchantWalletAddress": "0xYourWalletAddress",
    "amount": 29.99,
    "network": "base_sepolia"
  },
  "timestamp": "2026-03-01T12:05:00.000Z"
}

payment.failed:

{
  "event": "payment.failed",
  "data": {
    "sessionId": "665f1a2b3c4d5e6f7a8b9c0d",
    "reason": "TX_REVERTED"
  },
  "timestamp": "2026-03-01T12:05:00.000Z"
}

Handle duplicates. Bag may deliver the same event more than once. Use sessionId or txHash as an idempotency key to avoid fulfilling the same order twice.

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 to pay with and native tokens for gas fees.

WhatWhere to get it
Testnet USDC (Base Sepolia / Eth Sepolia)Circle USDC Faucet
Testnet USDC (Solana Devnet)Use devnet USDC mint 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU
Base Sepolia ETH (gas)Coinbase Faucet
Sepolia ETH (gas)Sepolia Faucet
Devnet SOL (gas)Solana Faucet

Walk through a payment

  1. Open the checkout URL in your browser: https://justusebag.xyz/pay/YOUR_PAYMENT_LINK_ID
  2. Connect a wallet (MetaMask, Coinbase Wallet, or Phantom for Solana) on the correct testnet
  3. Approve the USDC transfer — the checkout UI walks you through it
  4. Wait for on-chain confirmation — usually a few seconds on testnets
  5. Check your webhook server — you should see a payment.completed event logged
  6. 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: 665f1a2b3c4d5e6f7a8b9c0d
  Tx Hash: 0xabc123def456789...

Verification checklist

  • Payment link created successfully
  • Webhook server running and reachable (via ngrok or deployed)
  • Checkout page loads at https://justusebag.xyz/pay/YOUR_LINK_ID
  • Payment completes on testnet
  • Webhook received with valid signature
  • Transaction visible in dashboard

What's next


Quick reference

ResourceURL
Dashboardjustusebag.xyz
SDK (npm)npm install @getbagsapp/sdk
API Base URLhttps://justusebag.xyz
OpenAPI Specjustusebag.xyz/openapi.yaml

On this page