# Manage a subscription

A subscription is one purchase of a product and plan; it is the thing that renews and bills, and the [service instance](/concepts/model) is what runs. Buying is covered in [Subscriptions and billing](/concepts/billing). This page covers everything you do to a subscription afterwards. The samples use the helper from [Using the Zaroz API](/guides/api/overview).

## List and read

**curl**

```bash
curl -sS "$ZAROZ_API/subscriptions?project_id=$PROJECT&page_size=50" -H "$AUTH"
curl -sS "$ZAROZ_API/subscriptions/$SUBSCRIPTION" -H "$AUTH"
curl -sS "$ZAROZ_API/subscriptions/$SUBSCRIPTION/resources" -H "$AUTH"      # what it runs
```

**TypeScript**

```ts
import { call } from "./zaroz";

const list = await call("GET", `/subscriptions?project_id=${PROJECT}`);
for (const s of list.body.data) console.log(s.id, s.status, s.auto_renew, s.cancels_at, s.upcoming_charge);
```

**Python**

```python
from zaroz import call

_, subs = call("GET", f"/subscriptions?project_id={PROJECT}")
for s in subs["data"]:
    print(s["id"], s["status"], s["auto_renew"], s["cancels_at"], s["upcoming_charge"])
```

`status` is `pending`, `active`, `suspended`, `pending_termination`, `terminated`, `cancelled` or `failed`. A **suspended** subscription (usually unpaid) is stopped until the invoice is settled; paying it lifts the suspension.

## Stop or resume renewal

Renewal and cancellation are different on purpose.

- **`PUT /subscriptions/{id}/renewal`** decides whether the **next invoice** is raised. The service keeps running and no end date is set.
- **`PUT /subscriptions/{id}/cancellation`** decides whether the service **ends** when the paid period does.

**curl**

```bash
curl -sS -X PUT "$ZAROZ_API/subscriptions/$SUBSCRIPTION/renewal" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"auto_renew": false}'

curl -sS -X PUT "$ZAROZ_API/subscriptions/$SUBSCRIPTION/cancellation" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"cancel_at_period_end": true}'
# {"subscription_id":"...","status":"active","cancels_at":"2026-11-30T00:00:00Z","auto_renew":true,"changed":true}

# changed your mind before that instant:
curl -sS -X PUT "$ZAROZ_API/subscriptions/$SUBSCRIPTION/cancellation" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"cancel_at_period_end": false}'
```

**TypeScript**

```ts
await call("PUT", `/subscriptions/${SUB}/renewal`, { auto_renew: false });

const c = await call("PUT", `/subscriptions/${SUB}/cancellation`, { cancel_at_period_end: true });
console.log(c.body.cancels_at, c.body.changed); // changed:false means it was already so; not an error
```

**Python**

```python
call("PUT", f"/subscriptions/{SUB}/renewal", {"auto_renew": False})

_, c = call("PUT", f"/subscriptions/{SUB}/cancellation", {"cancel_at_period_end": True})
print(c["cancels_at"], c["changed"])   # changed False means it was already so; not an error
```

Scheduling an end **moves no money**: no invoice is raised or voided and nothing is credited. Sending the decision already in force succeeds with `changed: false`. A suspended subscription cannot be cancelled at the end of its period (`409`) because that would put a service stopped for non-payment back into service; settle what is owed first. These calls need the role that may buy for the organization (Owner or Admin); an organization token needs `subscriptions.renewal.manage` or `subscriptions.cancel`.

## Cancel now and get the unused time back

If you would rather stop immediately and turn the unused part of the paid period into wallet credit, take a quote and redeem it. The quote is signed and valid for ten minutes.

**curl**

```bash
curl -sS "$ZAROZ_API/subscriptions/$SUBSCRIPTION/wallet-credit-quote" -H "$AUTH"
# {"eligible": true, "total_cents": 1840, "minimum_cents": 100, "below_minimum": false,
#  "quote_id": "...", "expires_at": "...", "period_remaining_seconds": 1234567, ...}

curl -sS -X POST "$ZAROZ_API/subscriptions/$SUBSCRIPTION/cancel-with-wallet-credit" -H "$AUTH" \
  -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" -d '{"quote_id": "<quote_id>"}'
# {"status": "cancelled", "total_credited_cents": 1840, "credit_note_ids": ["..."], ...}
```

**TypeScript**

```ts
import { randomUUID } from "node:crypto";

const q = await call("GET", `/subscriptions/${SUB}/wallet-credit-quote`);
if (q.body.eligible && !q.body.below_minimum) {
  const done = await call("POST", `/subscriptions/${SUB}/cancel-with-wallet-credit`,
    { quote_id: q.body.quote_id }, { "Idempotency-Key": randomUUID() });
  console.log("credited", done.body.total_credited_cents);
} else {
  console.log("not eligible:", q.body.reason);
}
```

**Python**

```python
import uuid

_, q = call("GET", f"/subscriptions/{SUB}/wallet-credit-quote")
if q["eligible"] and not q["below_minimum"]:
    _, done = call("POST", f"/subscriptions/{SUB}/cancel-with-wallet-credit",
                   {"quote_id": q["quote_id"]}, headers={"Idempotency-Key": str(uuid.uuid4())})
    print("credited", done["total_credited_cents"])
else:
    print("not eligible:", q["reason"])
```

This **cancels the service now and cannot be undone**. It is refused with `422` for an altered or foreign quote, an ineligible subscription or a credit under the minimum, and with `409` for an expired quote or one whose amount changed since you took it (ask for a new quote). `GET` the quote as often as you like: it writes nothing.

## Resize

A resize names the **full target shape** (cpu, memory in GiB and storage in GiB), not a change. Send the dimension you do not want to change at its current value. Storage can only grow. Always quote first.

**curl**

```bash
BODY='{"resources": {"cpu": 2, "memory_gb": 4, "storage_gb": 40}}'

curl -sS -X POST "$ZAROZ_API/subscriptions/$SUBSCRIPTION/resize/quote" -H "$AUTH" -H "Content-Type: application/json" -d "$BODY"
# {"billing": {"kind": "growth", "subtotal_cents": ..., "tax_cents": ..., "total_cents": ..., "currency": "EUR"},
#  "dimensions": [{"dimension": "cpu", "current": 1, "requested": 2, "growing": true, "available": true}, ...]}

curl -sS -X POST "$ZAROZ_API/subscriptions/$SUBSCRIPTION/resize" -H "$AUTH" -H "Content-Type: application/json" -d "$BODY"
# {"outcome": "...", "invoice": {"id": "...", "amount_cents": ..., "status": "pending"}, "scheduled_downsize": null}

curl -sS -X POST "$ZAROZ_API/subscriptions/$SUBSCRIPTION/resize/cancel" -H "$AUTH"      # 204, withdraw a pending resize
```

**TypeScript**

```ts
const target = { resources: { cpu: 2, memory_gb: 4, storage_gb: 40 } };
const quote = await call("POST", `/subscriptions/${SUB}/resize/quote`, target);
if (quote.body.dimensions.some((d: any) => d.growing && !d.available)) throw new Error("no capacity right now");
console.log(quote.body.billing); // { kind: "growth", total_cents, ... } or { kind: "downsize" }

const resize = await call("POST", `/subscriptions/${SUB}/resize`, target);
if (resize.body.invoice) {
  // pay it: see Wallet, top-ups, codes and invoices
  await call("POST", `/billing/invoices/${resize.body.invoice.id}/pay-with-wallet`, undefined, { "Idempotency-Key": randomUUID() });
}
```

**Python**

```python
target = {"resources": {"cpu": 2, "memory_gb": 4, "storage_gb": 40}}
_, quote = call("POST", f"/subscriptions/{SUB}/resize/quote", target)
if any(d["growing"] and not d["available"] for d in quote["dimensions"]):
    raise RuntimeError("no capacity right now")
print(quote["billing"])  # {"kind": "growth", "total_cents": ...} or {"kind": "downsize"}

_, resize = call("POST", f"/subscriptions/{SUB}/resize", target)
if resize.get("invoice"):
    call("POST", f"/billing/invoices/{resize['invoice']['id']}/pay-with-wallet",
         headers={"Idempotency-Key": str(uuid.uuid4())})
```

How it is billed:

- **Growing** a dimension reserves capacity for **30 minutes** and raises a pending *resize* invoice for the prorated difference. The growth applies when that invoice is paid. Pay within the reservation or the hold is released.
- **Shrinking** is never billed and never credited. It is scheduled (`scheduled_downsize`) and applies at the end of the current billing period, right before the next renewal is priced at the smaller shape. A new request replaces an earlier scheduled reduction.
- The quote's stock check is a live preview and can be stale by the time you resize.
- `POST .../resize/cancel` withdraws a pending resize (voiding its unpaid invoice) and answers `204` even when nothing was pending. It is refused with `409` only when the pending growth has already been paid.

Resizing needs the Owner or Admin role; an organization token needs `subscriptions.resize.manage`. The product's minimum and maximum apply.

## Change plan

Where the product offers it, you can move to another of its catalog plans with the same quote-then-confirm shape. Get the plan ids from `GET /catalog/products/{id}/plans`.

**curl**

```bash
curl -sS -X POST "$ZAROZ_API/subscriptions/$SUBSCRIPTION/resize/plan-change/quote" -H "$AUTH" \
  -H "Content-Type: application/json" -d '{"plan_id": 7}'
curl -sS -X POST "$ZAROZ_API/subscriptions/$SUBSCRIPTION/resize/plan-change" -H "$AUTH" \
  -H "Content-Type: application/json" -d '{"plan_id": 7}'
```

**TypeScript**

```ts
const plans = await call("GET", `/catalog/products/${PRODUCT}/plans`);
const q = await call("POST", `/subscriptions/${SUB}/resize/plan-change/quote`, { plan_id: plans.body[0].id });
```

**Python**

```python
_, plans = call("GET", f"/catalog/products/{PRODUCT}/plans")
_, q = call("POST", f"/subscriptions/{SUB}/resize/plan-change/quote", {"plan_id": plans[0]["id"]})
```

It is refused if the product does not offer a self-service plan change, or if `plan_id` is not one of its active, public plans. Billing is the same as a resize: growth is invoiced now, a smaller plan takes effect at the end of the period.

## Free trials

A trial is a free period on a product. `GET /me/trials` lists what you may claim, including trials you already used (as `claimable: false`, so they do not silently disappear).

**curl**

```bash
curl -sS "$ZAROZ_API/me/trials" -H "$AUTH"
curl -sS -X POST "$ZAROZ_API/me/trials" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"trial_price_id": "<id from the list>", "name": "My trial", "slug": "my-trial"}'
```

**TypeScript**

```ts
const trials = await call("GET", "/me/trials");
const t = trials.body.trials.find((x: any) => x.claimable);
if (t) {
  const claimed = await call("POST", "/me/trials", { trial_price_id: t.id, name: "My trial", slug: "my-trial" });
  console.log(claimed.body.subscription.id);
}
```

**Python**

```python
_, trials = call("GET", "/me/trials")
t = next((x for x in trials["trials"] if x["claimable"]), None)
if t:
    _, claimed = call("POST", "/me/trials", {"trial_price_id": t["id"], "name": "My trial", "slug": "my-trial"})
    print(claimed["subscription"]["id"])
```

- Claiming creates the subscription in your **personal organization** (you cannot name another) and activates it at once. Nothing is charged and no payment method is needed.
- **`auto_renew` starts `false`**: do nothing and the trial is suspended at `trial_ends_at` instead of billing you. To keep the service, turn renewal on (`PUT .../renewal`) before then; billing starts at `trial_ends_at`.
- **One trial per product per account, permanently.** A second claim is refused with `already_claimed`; `capacity_reached` means try later; `not_available` means the trial is off.
- When `region_is_fixed` is true the trial pins its own hardware and ignores any region you send.
- Where the deployment uses a captcha for trials, the claim also needs a captcha token.
