# Subscriptions

Recurring billing on a customer's saved card. You pick a plan, name the customer, and we
take the opening charge and every renewal after it, running fraud screening and routing on
each one. 3-D Secure runs only while the buyer is present: renewals are merchant-initiated and
are charged without it.

```
POST   https://api.worthypayments.com/v1/plans
GET    https://api.worthypayments.com/v1/plans
GET    https://api.worthypayments.com/v1/plans/{id}
POST   https://api.worthypayments.com/v1/subscriptions
GET    https://api.worthypayments.com/v1/subscriptions
GET    https://api.worthypayments.com/v1/subscriptions/{id}
PATCH  https://api.worthypayments.com/v1/subscriptions/{id}
DELETE https://api.worthypayments.com/v1/subscriptions/{id}
```

| Scope | Grants |
|-------|--------|
| `plans:read` | List and fetch plans |
| `plans:write` | Create a plan, **server-only**, a `pk_` is refused |
| `subscriptions:read` | List and fetch subscriptions |
| `subscriptions:write` | Start, modify and cancel, **server-only**, a `pk_` is refused |

---

## Before you start

You need two things, and only one of them comes from the API.

**A plan.** Create one with `POST /v1/plans` (name, amount in cents, interval
`month`/`year`/`week`/`day`) or in the dashboard under **Billing → Plans**. Both write
the same object; `GET /v1/plans` lists it either way.

**A customer.** With a stored payment method, create charges immediately. That first
charge is not yet a listable invoice, calendar renewal and [test
bill-now](#advance-a-test-subscription) are. Without a card, create returns `status:
"incomplete"` and a `latest_invoice.payment_intent.client_secret` you pass to the existing
[Payment Element](./08b-payment-element.md), same iframe, no `plan` / `interval` on the
Element. See [Customers & Saved Cards](./18-customers.md) for attaching a card up front.

---

## 1. Create a plan

```bash
curl -X POST https://api.worthypayments.com/v1/plans \
  -H "x-api-key: sk_test_xxx" \
  -H "Content-Type: application/json" \
  -d '{"name":"Pro","amount":1999,"currency":"usd","interval":"month"}'
```

```json
{
  "id": "plan_2f9d1a77-6c30-4be1-9a54-0f7c8b21d4e3",
  "object": "plan",
  "name": "Pro",
  "amount": 1999,
  "currency": "usd",
  "interval": "MONTH",
  "interval_count": 1
}
```

`GET /v1/plans` lists it. Dashboard **Billing → Plans** writes the same object.

## 2. Resolve the plan

```bash
curl "https://api.worthypayments.com/v1/plans?active=true" \
  -H "x-api-key: sk_test_xxx"
```

```json
{
  "object": "list",
  "data": [
    {
      "id": "plan_2f9d1a77-6c30-4be1-9a54-0f7c8b21d4e3",
      "object": "plan",
      "name": "Pro monthly",
      "amount": 2900,
      "currency": "usd",
      "interval": "MONTH",
      "interval_count": 1,
      "trial_days": 14,
      "active": true,
      "archived": false,
      "subscription_count": 42
    }
  ],
  "has_more": false
}
```

`amount` is **minor units** like everywhere else on this API, `2900` is $29.00. The
billing period is `interval` × `interval_count`, so a quarterly plan is `MONTH` × `3`.

Archived plans are hidden unless you pass `include_archived=true`. An archived plan
cannot take a new subscription, so an id from that list is one `POST /v1/subscriptions`
will refuse.

`id` is the exact value the subscription object reports as its `plan`, so you can match
the two directly.

---

## 3. Start the subscription

`POST /subscriptions`

| Field | Required | Notes |
|-------|:---:|-------|
| `customer` | Yes | `cus_...`. Missing it is `400 parameter_invalid`. |
| `plan` | Yes | `plan_...` from step 1. The bare uuid is accepted too. |
| `quantity` | No | Seats / units. Defaults to `1`. |
| `coupon` | No | A coupon id to apply. |
| `default_payment_method` | No | `pm_...`. Omitted, the customer's default is used. |
| `collection_method` | No | `CHARGE_AUTOMATICALLY` (default, omit it) charges the saved card and writes a **PAID** invoice on **renewal**. `SEND_INVOICE` emails an OPEN invoice on renewal. Opening create still charges when a card is on file and does not mint the first invoice (see [Invoices](./18c-invoices.md).) |
| `payment_behavior` | No | `default_incomplete`, skip the opening charge even when a card is on file and return the Payment Element secret. Omit it to charge a saved card immediately. |
| `Idempotency-Key` | **Yes** | Header. See below. |

```bash
curl https://api.worthypayments.com/v1/subscriptions \
  -H "x-api-key: sk_test_xxx" \
  -H "Idempotency-Key: sub_order_1234" \
  -H "Content-Type: application/json" \
  -d '{
    "customer": "cus_9fK2pQ",
    "plan": "plan_2f9d1a77-6c30-4be1-9a54-0f7c8b21d4e3",
    "payment_behavior": "default_incomplete"
  }'
```

> **This call moves money when the customer already has a card and you omit
> `payment_behavior`.** Unless the plan is trialing, the opening charge is taken as part
> of creation, which is why it needs a real [`Idempotency-Key`](./12-idempotency.md) keyed
> on your order, not a fresh UUID per click. A retried `POST` must resolve to the same
> subscription rather than starting a second one on the same card. For the same reason a
> `FRAUD_ONLY` key can never reach this route; see [Integration
> modes](./25-integration-modes.md).  **`payment_behavior=default_incomplete`, or no card
> on file:** the same `POST` answers `status: "incomplete"` and
> `latest_invoice.payment_intent.client_secret`. Confirm that secret with
> `mountPaymentElement` (same as a one-off intent). Completing the Element takes the
> opening charge and activates the subscription. There is still no listable invoice on
> `GET /v1/invoices` until a later renewal. Do not put `plan` or `interval` on the
> Element.

The incomplete response (the Element loop):

```json
{
  "id": "sub_8c1e...",
  "object": "subscription",
  "status": "incomplete",
  "customer": "cus_9fK2pQ",
  "plan": "plan_2f9d1a77-6c30-4be1-9a54-0f7c8b21d4e3",
  "quantity": 1,
  "latest_invoice": {
    "object": "invoice",
    "payment_intent": {
      "object": "payment_intent",
      "status": "requires_payment_method",
      "client_secret": "pi_abc_secret_sess_elem_1"
    }
  },
  "cancel_at_period_end": false
}
```

`next_billing_at` is the field to store: it is when money moves next.

---

## Advance a test subscription

There is no Test Clock. In **test mode only**, run the calendar renewal now:

```
POST https://api.worthypayments.com/v1/test/subscriptions/{id}/bill
```

Same function the renew worker uses. `CHARGE_AUTOMATICALLY` (default) charges the saved
card for the next period **and** writes a **PAID** invoice listed on `GET /v1/invoices`.
`SEND_INVOICE` finalizes and emails an OPEN invoice, no card charge. A LIVE key is refused
with `400 test_mode_only` before billing is touched. Scope `subscriptions:write`.
[`Idempotency-Key`](./12-idempotency.md) is required. A new key per advance.

The charge id is not the invoice id. A decline leaves the sub `past_due` and emits
`subscription.past_due`. There is no `invoice.payment_failed` event.

Retries of a declined renewal are ours only for subscriptions we bill. If your own system
schedules renewals (a WooCommerce Subscriptions store, your own billing job) and charges a
saved card with [`POST /v1/charges`](./03-charges.md#charge-a-saved-card), those renewals
are yours to retry: we decline once and never retry them on our own, so the buyer is never
charged by two retry schedules for one period.

```bash
curl -X POST https://api.worthypayments.com/v1/test/subscriptions/sub_8c1e.../bill \
  -H "x-api-key: sk_test_xxx" \
  -H "Idempotency-Key: bill_sub_8c1e_period_2"
```

---

## 4. Modify one

`PATCH /subscriptions/{id}`, **exactly one intention per call.**

| Field | Effect |
|-------|--------|
| `plan` | Move to a different plan. Upstream prorates. |
| `paused: true` \| `false` | Suspend or resume billing. Optionally `resumes_at` (ISO-8601) with `true` to schedule the resume. |
| `cancel_at_period_end: true` | Schedule a cancellation for the end of the current period. Optionally `reason`. |

Setting two of them is a `400`, not a guess: each routes to a different operation with
different money semantics (a plan change prorates, a pause suspends billing) and there is
no defined order for combining them.

> **`cancel_at_period_end: false` is refused, not accepted as a no-op.** There is no
> uncancel: taking `false` would read as "this un-cancelled" while the subscription still
> lapsed at period end. To keep the customer, start a new subscription.

Unlike `POST`, `PATCH` takes **no** `Idempotency-Key`. These are state transitions
expressed as the state you want, so repeating one lands in the same place (pausing a
paused subscription is a no-op) and a claim would only let a legitimate correction be
swallowed as a duplicate.

---

## 5. Cancel

`DELETE /subscriptions/{id}`

```json
{ "immediately": true, "reason": "customer_request" }
```

Without `immediately`, the subscription runs to the end of the paid period and stops
there. With it, it ends now. Neither refunds the period already charged, issue a
[refund](./06-refunds.md) if you owe one.

---

## Reconciling

Poll nothing. The lifecycle arrives on [webhooks](./16-webhooks.md):

| Event | Meaning |
|-------|---------|
| `subscription.created` | Started, opening charge taken |
| `subscription.renewed` | A period was billed |
| `subscription.updated` | Plan, quantity or pause state changed |
| `subscription.past_due` | A renewal failed; retries are in progress |
| `subscription.suspended` | Retries were exhausted |
| `subscription.canceled` | Ended |
| `subscription.usage_exceeded` | A metered plan passed its limit |

`subscription.past_due` is the one to build for. There is **no `invoice.payment_failed`**.
A renewal failing is normal (cards expire) and the customer keeps their access until
`subscription.suspended` says otherwise. Treating `past_due` as cancellation cuts off
people who are about to pay.

---

## What is not here

- **Editing a plan.** Dashboard only. Create is `POST /v1/plans`.
- **An uncancel.** See the `PATCH` note.
- **Invoice create / finalize / pay / void.** List and retrieve are on
  `GET /v1/invoices`. Email an OPEN invoice with `POST /v1/invoices/{id}/send`
  (`invoices:write`). Finalize, pay and void stay in the dashboard. Auto-charge
  **renewals** mint a PAID invoice; opening create does not. See
  [Invoices](./18c-invoices.md).
