A subscription is one purchase of a product and plan; it is the thing that renews and bills, and the service instance is what runs. Buying is covered in Subscriptions and billing. This page covers everything you do to a subscription afterwards. The samples use the helper from Using the Zaroz API.
List and read
curl
Code
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
Code
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
Code
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
Code
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
Code
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
Code
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.
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.
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.
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
Code
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"}'
_, 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.