# 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.

Source: https://docs.layerhop.com/guides/subscriptions

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](/guides/invoices) 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](/guides/dashboard-and-api-keys) or in the [dashboard](https://dashboard.layerhop.com) (**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

```bash
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:

```ts
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](/guides/payment-buttons) 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`.

```ts
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](/guides/checkout#1-create-a-session). 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](/guides/invoices#2-manage-invoices) 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](/guides/invoices#4-emails-and-receipts).

## 4. Webhooks

Subscribe a [webhook endpoint](/guides/webhooks) 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](/guides/invoices#5-webhooks); 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](/guides/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](/guides/test-mode), 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`.
