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 orderInstall and configure
npm install @tbagtapp/sdkCreate 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_hereCreate 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