Zaroz
Billing and wallet

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

TerminalCode
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

TypeScriptCode
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

TerminalCode
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

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

curl

TerminalCode
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

TypeScriptCode
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

Code
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

TerminalCode
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

TypeScriptCode
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

Code
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

TerminalCode
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

TypeScriptCode
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

Code
_, 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

TerminalCode
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

TypeScriptCode
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

Code
_, 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.
Last modified on