LayerHopDocsLaunch app (opens in a new tab)

Subscriptions

Recurring billing in USDC or USDT. LayerHop issues and emails an invoice every period, the customer pays it with any token on any chain, and unpaid subscriptions get reminders before they are canceled.

A subscription bills one customer a fixed amount every day, week, month or year, in the stablecoin and on the chain you want to be paid in. A crypto wallet can't be charged automatically the way a card can, so LayerHop does the next best thing: at the start of every period it issues an invoice for that period and emails the customer a pay link. The customer pays it on the hosted invoice page with any token on any chain, and the subscription stays active. If an invoice goes unpaid, the subscription becomes past_due, the customer gets reminders, and after a grace period it is canceled.

Subscriptions are non-custodial like every LayerHop payment: nothing is pre-authorized or held, and each payment goes from the customer's wallet straight to your payout address.

How it works#

  1. Create a subscription with an API key or in the dashboard (Payments → Subscriptions): the customer's email, the price per period and the interval. You get subscription.created.
  2. Each period, LayerHop's billing issues that period's invoice (an ordinary invoice with subscriptionId set and the period's dates on its line item, for example Pro plan (2026-10-19 to 2026-11-19)), emails it from billing@layerhop.com and advances the period. You get subscription.renewed and invoice.sent.
  3. The customer pays on layerhop.com/invoice/{id} exactly like any invoice: you get invoice.paid and they get a receipt.
  4. If they don't pay by the due date, the subscription becomes past_due, reminders go out, and if the invoice is still unpaid after the grace period, the subscription is canceled.
Status Meaning
active Billing every period.
past_due An invoice is unpaid past its due date. Reminders are sent and the next renewal waits until it is paid.
paused You paused it: no invoices and no reminders until you resume.
canceled Final. You canceled it, or it stayed unpaid through the grace period.

Billing rules#

When What happens
startAt (default: when you create it) The first period starts and its invoice is issued and emailed (within about a minute).
Every period end The next period's invoice is issued and emailed.
Invoice issued + daysUntilDue (default 3) The invoice is due. Unpaid at that moment → past_due and subscription.past_due.
1 and 3 days after the due date Reminder emails with the pay link and the date the subscription will be canceled.
7 days after the due date Its open invoices are voided, the subscription is canceled for non-payment (cancellationReason: "unpaid") and you get subscription.canceled.
The overdue invoice is paid The subscription is active again. If the next period was due meanwhile, its invoice goes out right away.

Periods follow the start date. A monthly subscription that started on January 31 is billed on February 28 (or 29), March 31, April 30 and so on: the day of the month is kept and clamped to short months, so it never drifts. intervalCount stretches the period, for example "interval": "month", "intervalCount": 3 for quarterly billing.

A payment already on its way (the checkout session is processing) is never cut off: cancellation for non-payment waits until it settles.

1. Create a subscription#

Terminal
curl -X POST https://api.layerhop.com/v1/subscriptions \
  -H "x-api-key: $LAYERHOP_API_KEY" -H "content-type: application/json" \
  -d '{
    "customerEmail": "ada@example.com",
    "customerName": "Ada Lovelace",
    "merchantName": "Acme Studio",
    "description": "Pro plan",
    "amount": "49000000",
    "settleChain": "base",
    "settleToken": "USDC",
    "recipient": "0xYourPayoutAddress",
    "interval": "month",
    "metadata": { "plan": "pro" }
  }'

With the TypeScript SDK:

TypeScript
import { LayerHop } from "@layerhop/sdk";

const lh = new LayerHop({ apiKey: process.env.LAYERHOP_API_KEY });
const sub = await lh.subscriptions.create({
  customerEmail: "ada@example.com",
  description: "Pro plan",
  amount: "49000000", // 49 USDC per month
  settleChain: "base",
  settleToken: "USDC",
  recipient: "0xYourPayoutAddress",
  interval: "month",
});
console.log(sub.id, sub.nextInvoiceAt);

To bill a product you already defined, pass productId instead of amount, settleChain, settleToken and recipient: the product's price, settlement token, payout address and merchant name are used, and its name becomes the line item unless you pass a description.

TypeScript
await lh.subscriptions.create({ productId: "prod_01k7…", customerEmail: "ada@example.com", interval: "year" });
Field Notes
customerEmail Required. Where every invoice and reminder is emailed.
interval, intervalCount day, week, month or year, every 1 to 365 of them (default 1).
amount, settleChain, settleToken, recipient The price per period (an integer string in base units) and where you are paid, with the same supported pairs as checkout. Or productId.
description The invoice line item (required without productId).
customerName, merchantName, memo Shown on every invoice and email.
daysUntilDue Days after an invoice is issued that it is due, 0 to 60 (default 3).
startAt An ISO 8601 time to start later (up to 366 days ahead). Default: now.
metadata Up to 20 string keys, returned in the API and webhooks.

The response is the subscription: id (sub_…, or sub_test_… in test mode), status, amount, settlement, interval, intervalCount, daysUntilDue, currentPeriodStart and currentPeriodEnd (the period last invoiced), nextInvoiceAt, cancelAt and cancelAtPeriodEnd, canceledAt and cancellationReason, pausedAt, latestInvoiceId, metadata, createdAt and updatedAt.

2. Manage subscriptions#

Action API SDK
List (newest first, ?status=, customerEmail, limit, cursor) GET /v1/subscriptions lh.subscriptions.list({ status })
Get one GET /v1/subscriptions/{id} lh.subscriptions.get(id)
Its invoices (newest first) GET /v1/subscriptions/{id}/invoices lh.subscriptions.invoices(id)
Edit (from the next invoice) PATCH /v1/subscriptions/{id} lh.subscriptions.update(id, { … })
Cancel now or at period end POST /v1/subscriptions/{id}/cancel lh.subscriptions.cancel(id, { atPeriodEnd })
Pause POST /v1/subscriptions/{id}/pause lh.subscriptions.pause(id)
Resume POST /v1/subscriptions/{id}/resume lh.subscriptions.resume(id)

PATCH changes the customer, description, memo, amount, daysUntilDue or metadata; the change applies from the next invoice (invoices already sent keep their amount). The interval and the settlement token can't change: cancel and create a new subscription. The dashboard does the same under Payments → Subscriptions with the /v1/dashboard/subscriptions mirror of these endpoints.

Cancel#

  • Now (POST …/cancel with no body): the subscription is canceled at once and you get subscription.canceled. Invoices already sent stay open; void them if the customer shouldn't pay them.
  • At period end ({ "atPeriodEnd": true }): it stays active until currentPeriodEnd, then it is canceled instead of renewed. cancelAtPeriodEnd is true meanwhile; PATCH { "cancelAtPeriodEnd": false } undoes it.

Pause and resume#

Only an active subscription can be paused (pay or cancel a past_due one first). While paused, nothing is issued and no reminders are sent. On resume:

  • if the period already paid for hasn't ended, billing continues on the old schedule;
  • otherwise a new period starts now and its invoice goes out right away, so paused time is never billed.

Invoices still open from before the pause get a fresh due date (daysUntilDue from the resume), so the customer isn't past due the moment you resume.

3. Emails#

All emails come from billing@layerhop.com in your merchantName:

  • Each period's invoice, with the period on the line item, the due date, a note that the subscription renews every interval, and the pay link.
  • Reminders 1 and 3 days after the due date, with the date the subscription will be canceled if the invoice is still unpaid.
  • The receipt after each payment, as for any invoice.

4. Webhooks#

Subscribe a webhook endpoint to:

Event Sent when data
subscription.created You create a subscription. subscription
subscription.renewed A period's invoice was issued and emailed (also for the first period). subscription and invoice
subscription.past_due An invoice is unpaid past its due date. subscription and invoice (the overdue one)
subscription.canceled You canceled it, a scheduled cancel took effect, or it stayed unpaid through the grace period (cancellationReason). subscription
invoice.sent, invoice.paid As for every invoice; the invoice carries subscriptionId. invoice (and session)

Keep access to your service tied to invoice.paid (extend it to the invoice's period) and subscription.canceled (revoke it).

JSON
{
  "id": "evt_…",
  "type": "subscription.renewed",
  "livemode": true,
  "createdAt": "2026-11-19T10:00:04.000Z",
  "data": {
    "subscription": {
      "id": "sub_01k7v0c2c4x8q6r0v3b5n7p9d",
      "object": "subscription",
      "status": "active",
      "customerEmail": "ada@example.com",
      "description": "Pro plan",
      "amount": "49000000",
      "settlement": { "chainId": 8453, "token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "symbol": "USDC", "decimals": 6 },
      "interval": "month",
      "intervalCount": 1,
      "currentPeriodStart": "2026-11-19T10:00:00.000Z",
      "currentPeriodEnd": "2026-12-19T10:00:00.000Z",
      "nextInvoiceAt": "2026-12-19T10:00:00.000Z",
      "cancelAtPeriodEnd": false,
      "latestInvoiceId": "inv_01k8a…",
      "metadata": { "plan": "pro" }
    },
    "invoice": {
      "id": "inv_01k8a…",
      "number": "INV-0012",
      "status": "open",
      "amount": "49000000",
      "dueAt": "2026-11-22T10:00:04.000Z",
      "url": "https://layerhop.com/invoice/inv_01k8a…",
      "subscriptionId": "sub_01k7v0c2c4x8q6r0v3b5n7p9d"
    }
  }
}

The dashboard's Send test event and layerhop trigger subscription.renewed send realistic samples of all four events.

Fees#

There is no subscription fee. Each invoice is paid through checkout, so the same pricing applies as for any invoice: a payment in the settlement token on the settlement chain is a plain transfer with no LayerHop fee, and any other token or chain is routed with the standard LayerHop API fee included in what the customer sees before signing.

Test mode#

Create subscriptions with an lh_test_ key, or with Test mode on in the dashboard. Test subscriptions (sub_test_…) issue test invoices (inv_test_…) that are paid through the sandbox, with real emails marked Test. Webhooks go to your test endpoints only. To try the whole cycle quickly, create a daily subscription with "daysUntilDue": 0.

Last updated