Skip to main content

Documentation Index

Fetch the complete documentation index at: https://yanhgming.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

Marlin’s recurring billing works differently from card-based systems. Instead of storing payment credentials, the customer signs a single on-chain transaction that delegates token authority to the Marlin program for up to a specified spending cap. After that, Marlin charges automatically on schedule — no further action required from the customer. This guide walks through creating a plan, sharing the subscription checkout URL, handling lifecycle webhooks, and managing subscriptions via the API.
1

Install the SDK

npm install @marlin/sdk
# or
pnpm add @marlin/sdk
import { Marlin } from "@marlin/sdk";

const marlin = new Marlin({ apiKey: process.env.MARLIN_API_KEY! });
2

Create a subscription plan

A plan defines the billing amount, currency, and interval. The slug you provide becomes part of the hosted checkout URL, so choose something URL-safe.
const plan = await marlin.plans.create({
  name: "Pro Monthly",
  description: "Full access to all Pro features",
  currency: "USDC",
  amount: 4900,        // $49.00 in cents
  interval: "month",   // "day" | "week" | "month" | "year"
  intervalCount: 1,    // charge every 1 month
  trialPeriodDays: 14, // optional free trial
  metadata: {
    tier: "pro",
  },
});

console.log(plan.id);   // "plan_01HXYZ..."
console.log(plan.name); // "Pro Monthly"
trialPeriodDays delays the first charge by that many days. The customer still signs the authorization transaction immediately, but the Marlin program will not attempt the first charge until the trial ends.
3

Share the plan checkout URL

Every plan automatically has a hosted subscription checkout page. The URL follows this pattern:
https://checkout.marlin.fi/sub/{planSlug}
You can find the publicSlug on the plan object returned by the API, or set it in the dashboard. Share this URL anywhere — email, marketing page, in-app button — and the customer can subscribe with any Solana wallet.When the customer subscribes, they:
  1. Connect their wallet
  2. Choose a spending cap authorization period (6 months, 1 year, or 2 years)
  3. Sign one transaction on-chain
That single signature delegates token authority to the Marlin program up to the chosen cap. All future charges happen automatically without any further wallet interaction.
4

Handle subscription webhooks

Marlin posts events to your webhook endpoint as subscriptions move through their lifecycle. Use verifyWebhook to validate every incoming event before acting on it.
import { verifyWebhook } from "@marlin/sdk";
import type { Subscription } from "@marlin/sdk";

app.post("/webhooks/marlin", express.raw({ type: "application/json" }), (req, res) => {
  let event;

  try {
    event = verifyWebhook({
      payload: req.body,
      signature: req.headers["marlin-signature"] as string,
      secret: process.env.MARLIN_WEBHOOK_SECRET!,
    });
  } catch {
    return res.status(400).send("Invalid signature");
  }

  switch (event.type) {
    case "subscription.activated": {
      const sub = event.data as Subscription;
      // Provision access for sub.customerId
      await grantAccess(sub.customerId, sub.planId);
      break;
    }

    case "subscription.past_due": {
      const sub = event.data as Subscription;
      // Notify the customer and optionally restrict access
      await notifyPaymentFailed(sub.customerId);
      break;
    }

    case "subscription.canceled": {
      const sub = event.data as Subscription;
      // Revoke access at period end
      await scheduleAccessRevocation(sub.customerId, sub.currentPeriodEnd);
      break;
    }
  }

  res.status(200).send("OK");
});
The full set of subscription event types:
EventWhen it fires
subscription.createdCustomer completes the on-chain subscribe transaction
subscription.activatedFirst charge succeeds (or trial starts)
subscription.pausedSubscription is paused via API or by the customer
subscription.resumedSubscription resumes from paused state
subscription.past_dueA charge attempt fails
subscription.canceledSubscription is canceled
5

Manage subscriptions via API

You can pause, resume, or cancel any subscription programmatically. All three operations take effect immediately and fire the corresponding webhook event.
const subscriptionId = "sub_01HXYZ";

// Pause — stops future charges while preserving the subscription
const paused = await marlin.subscriptions.pause(subscriptionId);

// Resume — re-enables charging from the current period
const resumed = await marlin.subscriptions.resume(subscriptionId);

// Cancel — ends the subscription and revokes the delegate
const canceled = await marlin.subscriptions.cancel(subscriptionId);
To list all subscriptions for a customer:
const { data: subscriptions } = await marlin.subscriptions.list({
  customerId: "cus_01HXYZ",
  status: "active",
});

How automatic charging works

When a billing period is due, Marlin processes the charge automatically on-chain. Because the charge is triggered by a permissionless on-chain instruction, it proceeds independently — even if your server is temporarily unavailable. The payment splits atomically: 99.5% settles directly to your wallet, and 0.5% goes to the Marlin protocol fee. If a charge fails because the customer’s balance is insufficient or their authorization cap is reached, Marlin transitions the subscription to past_due and fires the subscription.past_due webhook.
Customers can revoke their token delegate at any time from their wallet, which will cause the next charge attempt to fail. Monitor subscription.past_due events and reach out to affected customers.