Bag is live โ€” accept USDC & card payments globally. Get started โ†’
BagBag Docs
Guides

Subscriptions

Integrate recurring billing with payment links, webhooks, and subscription management.

Integrate Subscriptions

This guide walks through implementing recurring billing with Bag โ€” from creating a subscription-enabled payment link to handling renewal webhooks and managing subscription lifecycle.


Create a payment link with a recurring billing cycle. Use the dashboard or API:

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

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

const link = await bag.paymentLinks.create({
  name: "Pro Plan - Monthly",
  amount: 29.99,
  network: "base",
  description: "Monthly Pro subscription with 14-day free trial",
});
curl -X POST https://getbags.app/api/payment-links \
  -H "Authorization: Bearer $BAG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Pro Plan - Monthly",
    "amount": 29.99,
    "network": "base",
    "description": "Monthly Pro subscription with 14-day free trial"
  }'

Configure billing cycle, trial period, and dunning behavior in the dashboard under the payment link settings.


Step 2: Handle the initial payment

When a customer subscribes, Bag creates a checkout session and processes the first payment (or starts the trial). You receive a subscription.created webhook:

{
  "event": "subscription.created",
  "data": {
    "subscriptionId": "sub_abc123",
    "paymentLinkId": "a1b2c3d4-...",
    "status": "active",
    "currentPeriodStart": "2026-04-01T00:00:00Z",
    "currentPeriodEnd": "2026-05-01T00:00:00Z",
    "customer": {
      "email": "jane@example.com",
      "walletAddress": "0x..."
    }
  }
}

Store the subscriptionId in your database and grant access to the customer.


Step 3: Handle renewals

Each billing cycle, Bag automatically charges the customer and sends a webhook:

app.post("/webhooks/bag", async (req, res) => {
  const { event, data } = req.body;

  switch (event) {
    case "subscription.renewed":
      await extendAccess(data.subscriptionId, data.currentPeriodEnd);
      break;

    case "subscription.past_due":
      await notifyCustomer(data.subscriptionId, "Payment failed, retrying...");
      break;

    case "subscription.canceled":
      await revokeAccess(data.subscriptionId);
      break;

    case "subscription.renewal_due":
      // Stablecoin subscriptions: customer may need to re-approve
      await notifyCustomer(data.subscriptionId, "Renewal coming up");
      break;
  }

  res.status(200).send("ok");
});

Step 4: Manage subscriptions

Pause a subscription

curl -X PATCH "https://getbags.app/api/subscriptions/$SUBSCRIPTION_ID" \
  -H "Authorization: Bearer $BAG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "paused"}'

Resume a subscription

curl -X PATCH "https://getbags.app/api/subscriptions/$SUBSCRIPTION_ID" \
  -H "Authorization: Bearer $BAG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "active"}'

Cancel a subscription

curl -X PATCH "https://getbags.app/api/subscriptions/$SUBSCRIPTION_ID" \
  -H "Authorization: Bearer $BAG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "canceled"}'

Step 5: Handle stablecoin re-approval

For USDC subscriptions, the customer approves a spending limit (e.g., 6 months). When the approval is about to expire, Bag sends a subscription.renewal_due webhook. Direct the customer to the re-approval page:

case "subscription.renewal_due":
  const reauthUrl = `https://getbags.app/reauth/${data.subscriptionId}`;
  await sendEmail(data.customer.email, {
    subject: "Action needed: renew your subscription approval",
    body: `Please re-approve your subscription: ${reauthUrl}`,
  });
  break;

Subscription webhook events

EventWhenYour action
subscription.createdNew subscription startsStore subscription, grant access
subscription.updatedPlan or details changedUpdate stored plan info
subscription.renewedPayment succeededExtend access period
subscription.renewal_dueRe-approval needed (stablecoin)Notify customer
subscription.past_duePayment failed, retryingWarn customer
subscription.canceledSubscription endedRevoke access

What's next

On this page