Bag is live — accept USDC & card payments globally. Get started →
BagBag Docs
SDKs

Integration Example

Build a Next.js checkout flow with the Bag SDK — create payment links, redirect to hosted checkout, and handle success.

Integration Example

This guide walks through building a complete checkout flow in Next.js using @tbagtapp/sdk. The pattern is the same as Stripe Checkout or Polar — your server creates a payment link, you redirect the customer to Bag's hosted checkout page, and they return to your site after paying.

This mirrors how the sdk-sample app works. You can clone it and run it locally to see the full flow.


The flow

Customer clicks "Buy" → Your server creates a payment link via SDK
→ Redirect to https://getbags.app/pay/{linkId}
→ Customer pays with USDC → Bag confirms on-chain
→ Customer returns to your success page
→ Webhook fires → You fulfill the order

Install and configure

npm install @tbagtapp/sdk

Create a shared SDK instance. This goes in a server-only file — never expose your API key to the browser.

// lib/bag.ts
import { Bag } from "@tbagtapp/sdk";

export const bag = new Bag({
  apiKey: process.env.BAG_API_KEY!,
});

Set the environment variable:

# .env.local
BAG_API_KEY=bag_test_sk_your_key_here

Create a checkout API route

When the customer clicks "Buy", your frontend calls this route. It creates a payment link and returns the checkout URL.

// app/api/checkout/route.ts
import { NextRequest, NextResponse } from "next/server";
import { bag } from "@/lib/bag";
import type { CreatePaymentLinkInput } from "@tbagtapp/sdk";

export async function POST(request: NextRequest) {
  const { name, amount, description } = await request.json();

  const origin = request.headers.get("origin") || "";

  const input: CreatePaymentLinkInput = {
    name,
    amount,
    network: "base_sepolia", // use "base" for production
    description,
    targetUrl: `${origin}/success`, // where to redirect after payment
  };

  const link = await bag.paymentLinks.create(input, {
    idempotencyKey: `checkout-${name}-${Date.now()}`,
  });

  const checkoutUrl = `https://getbags.app/pay/${link.id}`;

  return NextResponse.json({ checkoutUrl, linkId: link.id });
}

targetUrl must be HTTPS in production. In local development, omit it — the customer can navigate back manually.

Add a "Buy" button

The frontend calls your checkout route and redirects to Bag's hosted checkout page.

// app/page.tsx (client component)
"use client";

import { useState } from "react";

export default function StorePage() {
  const [loading, setLoading] = useState(false);

  async function handleBuy() {
    setLoading(true);
    try {
      const res = await fetch("/api/checkout", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          name: "Pro Plan",
          amount: 29.99,
          description: "Monthly subscription",
        }),
      });

      const { checkoutUrl } = await res.json();
      window.location.href = checkoutUrl; // redirect to Bag checkout
    } catch {
      alert("Checkout failed. Please try again.");
    } finally {
      setLoading(false);
    }
  }

  return (
    <button onClick={handleBuy} disabled={loading}>
      {loading ? "Redirecting..." : "Buy for $29.99"}
    </button>
  );
}

Create a success page

After payment, Bag redirects the customer to your targetUrl.

// app/success/page.tsx
export default function SuccessPage() {
  return (
    <div>
      <h1>Payment successful!</h1>
      <p>Thank you for your purchase.</p>
      <a href="/">Back to store</a>
    </div>
  );
}

Don't fulfill the order from the success page. The customer can reach this page without actually paying. Always use webhooks as the source of truth.

Build a merchant dashboard

Use the SDK to list payment links and transactions for your admin panel.

// app/api/links/route.ts
import { NextResponse } from "next/server";
import { bag } from "@/lib/bag";

export async function GET() {
  const { data } = await bag.paymentLinks.list({ limit: 50 });
  return NextResponse.json(data);
}
// app/api/transactions/route.ts
import { NextResponse } from "next/server";
import { bag } from "@/lib/bag";

export async function GET() {
  const { data } = await bag.transactions.list({ limit: 50 });
  return NextResponse.json(data);
}

Paginate through large result sets:

// Fetch all transactions
let cursor: string | undefined;
const all = [];

do {
  const page = await bag.transactions.list({
    limit: 100,
    starting_after: cursor,
  });
  all.push(...page.data);
  cursor = page.hasMore
    ? page.data[page.data.length - 1].id
    : undefined;
} while (cursor);

Handle errors

Wrap SDK calls in try/catch. The SDK throws BagError with structured error info.

import { BagError } from "@tbagtapp/sdk";

try {
  await bag.paymentLinks.create({ name: "", amount: -1, network: "base" });
} catch (err) {
  if (err instanceof BagError) {
    console.error(`[${err.statusCode}] ${err.message}`);
    console.error(`Error code: ${err.code}`);
    console.error(`Request ID: ${err.requestId}`); // for support tickets

    if (err.statusCode === 401) {
      // Invalid API key
    } else if (err.statusCode === 400) {
      // Validation error — check err.message
    }
  }
}

Full example structure

your-app/
├── .env.local          # BAG_API_KEY=bag_test_sk_...
├── lib/
│   └── bag.ts          # Shared Bag SDK instance
├── app/
│   ├── page.tsx        # Storefront with "Buy" buttons
│   ├── success/
│   │   └── page.tsx    # Post-payment landing page
│   ├── dashboard/
│   │   └── page.tsx    # Admin view of links + transactions
│   └── api/
│       ├── checkout/
│       │   └── route.ts    # Creates payment link, returns checkout URL
│       ├── links/
│       │   └── route.ts    # Lists payment links
│       └── transactions/
│           └── route.ts    # Lists transactions

What's next

On this page