# Sell subscriptions with coupons and plan changes

> Create subscription plans, send customers to subscribe, offer coupon codes, let them change plans, and keep access in your app in step with webhooks.

Source: https://anytime-capital.com/docs/sell-subscriptions · Markdown: https://anytime-capital.com/docs/sell-subscriptions.md · All docs: https://anytime-capital.com/docs/llms-full.txt

Customers subscribe on Anytime Capital's hosted page for each plan. Your app creates the plans, sends customers to them, and keeps access in step with webhooks. Coupons and plan changes are switched on in the dashboard; no code needed for those. Endpoint details: [Subscriptions & plans](https://anytime-capital.com/docs/subscriptions.md).

## 1. Create your plans

Make them in **Subscriptions → Plans** in the dashboard, or from code with a key that has `subscriptions` write. A plan's price and interval are fixed once it exists; for a new price, make a new plan.

**create-plans.mjs**

```javascript
// create-plans.mjs — run once: node create-plans.mjs
const API = "https://anytime-capital.com/api/v1";

const PLANS = [
  { key: "plan-basic-monthly", title: "Basic", amountCents: 1900, interval: "month", trialDays: 14 },
  { key: "plan-pro-monthly", title: "Pro", amountCents: 4900, interval: "month", trialDays: 14 },
  { key: "plan-pro-yearly", title: "Pro (yearly)", amountCents: 49000, interval: "year" },
];

for (const { key, ...plan } of PLANS) {
  const res = await fetch(`${API}/subscription-plans`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.ANYTIME_API_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": key, // run it twice within 24 hours and you still get one plan each
    },
    body: JSON.stringify(plan),
  });
  const created = await res.json();
  if (!res.ok) throw new Error(`${created.error.code}: ${created.error.message}`);
  console.log(created.id, created.title, created.url); // put each url behind its Subscribe button
}
```

## 2. Send customers to subscribe

Each plan's `url` is its subscribe page: link your **Subscribe** buttons to it. Free trials start there, and the subscription appears on your **Subscriptions** page and in `subscription.created`.

## 3. Offer coupons

1. Create a code on the **Discounts** page, or with `POST /api/v1/discount-codes` (see [Discount codes](https://anytime-capital.com/docs/discount-codes.md)).
2. In the dashboard, turn on **Use on subscriptions** for it and choose how long it lasts: the first payment, several months, or every payment, and which plans it covers.
3. The plan's subscribe page then shows **Have a discount code?**. You can also add a code to an existing subscriber from their details.

How long a discount lasts and new-customers-only rules are on [Subscription discounts](https://anytime-capital.com/docs/subscription-discounts.md); subscription settings for a code can't be set through the API.

## 4. Keep access in step

Subscribe your endpoint to `subscription.created`, `subscription.updated`, `subscription.paused`, `subscription.resumed`, `subscription.past_due` and `subscription.canceled`. Events can arrive more than once and out of order, so on each one, read the subscription as it is now and store that.

**subscriptions-webhook.mjs**

```javascript
// subscriptions-webhook.mjs — Node.js 18+ with Express: npm install express
import crypto from "node:crypto";
import express from "express";

const API = "https://anytime-capital.com/api/v1";
const secret = process.env.ANYTIME_WEBHOOK_SECRET; // whsec_…
const access = new Map(); // email → { plan, status, until }: use your database

/** Which statuses keep the product open. */
function hasAccess(status) {
  return ["trialing", "active", "canceling", "past_due"].includes(status); // past_due: renewal being retried
}

function verify(rawBody, header) {
  const parts = Object.fromEntries(String(header ?? "").split(",").map((part) => part.split("=")));
  const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  return (
    typeof parts.v1 === "string" &&
    parts.v1.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected)) &&
    Math.abs(Date.now() / 1000 - Number(parts.t)) <= 300
  );
}

/** Events can arrive out of order: always store the subscription as it is now. */
async function syncSubscription(id) {
  const res = await fetch(`${API}/subscriptions/${id}`, {
    headers: { Authorization: `Bearer ${process.env.ANYTIME_API_KEY}` },
  });
  const subscription = await res.json();
  if (!res.ok) throw new Error(`${subscription.error.code}: ${subscription.error.message}`);
  access.set(subscription.customer.email, {
    plan: subscription.plan.id,
    status: subscription.status,
    allowed: hasAccess(subscription.status),
    until: subscription.currentPeriodEnd,
  });
}

const app = express();
app.post("/webhooks/anytime", express.raw({ type: "application/json" }), async (req, res) => {
  const rawBody = req.body.toString("utf8");
  if (!verify(rawBody, req.get("Anytime-Signature"))) return res.status(400).send("Invalid signature");
  const event = JSON.parse(rawBody);
  if (event.type.startsWith("subscription.")) {
    try {
      await syncSubscription(event.data.id);
    } catch (error) {
      console.error(error);
      return res.sendStatus(500); // not 2xx, so the event is sent again later
    }
  }
  res.sendStatus(200);
});

app.listen(3000);
```

> **A nightly safety net:** Once a day, page through `GET /api/v1/subscriptions` and run each through the same sync. It catches anything a missed event would have changed, plan changes included.

## 5. Plan changes

Move a subscriber with **Change plan** from their details on the **Subscriptions** page, now (with the unused part of the period refunded) or at the next renewal. To let subscribers switch themselves, mark the plans they may move to and turn on **Subscribers can switch plans**. Plan changes aren't in the API: read `plan.id` from the subscription, never from what you remember. See [plan changes](https://anytime-capital.com/docs/subscription-discounts.md#changing-a-subscriber-s-plan).

## 6. Cancel, pause and resume from your app

**From your admin screen**

```javascript
// From your admin screen: end, pause or resume a subscriber (subscriptions:write).
async function subscriptionAction(id, action, body) {
  const res = await fetch(`https://anytime-capital.com/api/v1/subscriptions/${id}/${action}`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.ANYTIME_API_KEY}`,
      "Idempotency-Key": `${action}-${id}-${new Date().toISOString().slice(0, 10)}`,
      ...(body ? { "Content-Type": "application/json" } : {}),
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  const subscription = await res.json();
  if (!res.ok) throw new Error(`${subscription.error.code}: ${subscription.error.message}`); // e.g. CANNOT_PAUSE
  return subscription;
}

await subscriptionAction("sub_2208", "cancel", { atPeriodEnd: true }); // or { atPeriodEnd: false } to end now
await subscriptionAction("sub_2208", "resume"); // undoes the cancellation set for the period's end
```

The subscriber gets the same emails as when you act from the dashboard. Errors such as `409 CANNOT_PAUSE` or `409 SUBSCRIPTION_ENDED` explain themselves: see the [error code catalogue](https://anytime-capital.com/docs/error-codes.md).

- [Bill usage with meters](https://anytime-capital.com/docs/bill-usage-with-meters.md): Charge for what subscribers use on top of the plan.
- [Verify webhooks](https://anytime-capital.com/docs/webhook-signatures.md): The signature check in Node.js, Python and PHP.

---

Anytime Capital is a fintech company, not a bank. Payment processing, banking and payout services are provided by Anytime Capital's licensed partners. Rates shown are for US card payments; international cards, currency conversion and disputes may carry additional fees shown in your dashboard before you accept a payment.
