Zaroz
Billing and wallet

Wallet, top-ups, codes and invoices

Money in Zaroz is two things: invoices (what you owe) and a wallet (credit you hold). How a purchase turns into an invoice is in Subscriptions and billing. This page is the money side, call by call. The samples use the helper from Using the Zaroz API. All amounts are integer cents in the invoice's currency.

Read the wallet

An organization owns its wallet, and every member of it reads the same balance. Your own wallet is the one of your personal organization.

curl

TerminalCode
curl -sS "$ZAROZ_API/me/wallet" -H "$AUTH" curl -sS "$ZAROZ_API/organizations/$ORG/wallet" -H "$AUTH" curl -sS "$ZAROZ_API/me/wallet/entries?page=1&page_size=25" -H "$AUTH" # the ledger, newest first
JSONCode
{ "balances": [ { "currency": "EUR", "amount_cents": 3000, "real_cents": 2500, "promo_cents": 500, "legacy_cents": 0, "spendable_cents": 2500 } ] }

TypeScript

TypeScriptCode
import { call } from "./zaroz"; const wallet = await call("GET", "/me/wallet"); for (const b of wallet.body.balances) { console.log(b.currency, "spendable", b.spendable_cents / 100, "promo", b.promo_cents / 100); } const ledger = await call("GET", "/me/wallet/entries?page_size=25"); for (const e of ledger.body.data) console.log(e.created_at, e.reason, e.amount_cents);

Python

Code
from zaroz import call _, wallet = call("GET", "/me/wallet") for b in wallet["balances"]: print(b["currency"], "spendable", b["spendable_cents"] / 100, "promo", b["promo_cents"] / 100) _, ledger = call("GET", "/me/wallet/entries?page_size=25") for e in ledger["data"]: print(e["created_at"], e["reason"], e["amount_cents"])

Only non-zero balances are returned, so a wallet that was never funded answers an empty balances list. Treat a missing currency as zero.

What the figures mean

FieldMeaning
spendable_centsWhat a wallet payment can use right now. Use this to decide whether an invoice can be paid, not amount_cents
real_centsCash-like money: top-ups, refunds settled to the wallet
promo_centsPromotional credit. It is never cash and cannot be withdrawn. It is not drawn like cash by a payment: it is applied automatically as a discount line when an invoice is issued (and when an unpaid invoice is paid from the wallet)
legacy_centsOlder money not yet classified; not spendable by a wallet payment
amount_centsAll of the above added together. For reconciliation only

The ledger has no running balance column: each entry is a signed movement (positive credits, negative debits) with a reason such as top_up_pay_pal, redeemed_code, invoice_autopay, invoice_refund or promo_grant.

Add funds

First ask which providers can take a top-up right now, and show only those:

curl

TerminalCode
curl -sS "$ZAROZ_API/me/wallet/top-ups/availability" -H "$AUTH" # {"paypal": true, "nowpayments": false} curl -sS -X POST "$ZAROZ_API/me/wallet/top-ups" -H "$AUTH" -H "Content-Type: application/json" \ -d '{"amount_cents": 2500, "provider": "paypal"}' # {"id": "...", "status": "waiting", "checkout_url": "https://...", "expires_at": "..."} curl -sS "$ZAROZ_API/me/wallet/top-ups/$TOPUP" -H "$AUTH" # poll until "credited"

TypeScript

TypeScriptCode
const topup = await call("POST", "/me/wallet/top-ups", { amount_cents: 2500, provider: "paypal" }); console.log("Send the customer to", topup.body.checkout_url); // nothing is credited until the provider confirms; poll for (;;) { const t = await call("GET", `/me/wallet/top-ups/${topup.body.id}`); if (["credited", "expired", "failed", "review_required", "reversed"].includes(t.body.status)) { console.log(t.body.status); break; } await new Promise((r) => setTimeout(r, 5000)); }

Python

Code
import time from zaroz import call _, topup = call("POST", "/me/wallet/top-ups", {"amount_cents": 2500, "provider": "paypal"}) print("Send the customer to", topup["checkout_url"]) while True: # nothing is credited until the provider confirms _, t = call("GET", f"/me/wallet/top-ups/{topup['id']}") if t["status"] in ("credited", "expired", "failed", "review_required", "reversed"): print(t["status"]) break time.sleep(5)
  • provider is paypal or nowpayments (cryptocurrency).
  • amount_cents must be one of the fixed EUR presets: 2500, 5000, 10000, 25000, 100000. Anything else is refused with invalid-field-value.
  • The call credits nothing. The wallet is credited once, when the provider confirms the payment. Only credited means money arrived; expired, failed, review_required and reversed are terminal.
  • Retrying a top-up after a timeout starts a second, separate checkout. Neither moves money until it is paid.
  • A provider that is not set up or does not answer is a 503 and nothing was charged.
  • An organization token needs organizations.wallet.top_up for this.

Redeem a wallet code

A wallet code (a gift or giveaway code) credits one wallet, once. preview needs no account, so a claim link can show what the code is worth before anyone signs in.

curl

TerminalCode
curl -sS "$ZAROZ_API/public/wallet/codes/ABCD-EFGH/preview" # {"amount_cents": 1000, "currency": "EUR", "expires_at": null} curl -sS -X POST "$ZAROZ_API/me/wallet/codes/redeem" -H "$AUTH" -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" -d '{"code": "ABCD-EFGH"}' # {"amount_cents": 1000, "currency": "EUR", "entry_id": "..."}

TypeScript

TypeScriptCode
import { randomUUID } from "node:crypto"; const redeemed = await call("POST", "/me/wallet/codes/redeem", { code: "ABCD-EFGH" }, { "Idempotency-Key": randomUUID(), }); console.log(redeemed.body.amount_cents, redeemed.body.currency);

Python

Code
import uuid from zaroz import call status, redeemed = call("POST", "/me/wallet/codes/redeem", {"code": "ABCD-EFGH"}, headers={"Idempotency-Key": str(uuid.uuid4())}) print(redeemed)

Whatever the reason a code fails (it does not exist, was used, was revoked, expired, or a per-person limit was reached), the answer is the same invalid-field-value. That is deliberate: it stops anyone from probing which codes exist. Always send an Idempotency-Key, so a retry after a timeout returns the first credit instead of a refusal.

Invoices

curl

TerminalCode
curl -sS "$ZAROZ_API/billing/invoices?page=1&page_size=25" -H "$AUTH" # every organization you may see curl -sS "$ZAROZ_API/organizations/$ORG/invoices?status=pending" -H "$AUTH" curl -sS "$ZAROZ_API/billing/invoices/$INVOICE" -H "$AUTH" # with its lines curl -sS "$ZAROZ_API/billing/invoices/open-summary" -H "$AUTH" # count, overdue, total owed curl -sS "$ZAROZ_API/subscriptions/$SUBSCRIPTION/invoices" -H "$AUTH"

TypeScript

TypeScriptCode
const open = await call("GET", "/billing/invoices/open-summary"); console.log(`${open.body.count} open, ${open.body.overdue_count} overdue, total ${open.body.total_cents}`); const invoice = await call("GET", `/billing/invoices/${INVOICE}`); for (const line of invoice.body.lines) console.log(line.description, line.amount_cents, line.project_name);

Python

Code
_, open_ = call("GET", "/billing/invoices/open-summary") print(open_["count"], "open,", open_["overdue_count"], "overdue, total", open_["total_cents"]) _, invoice = call("GET", f"/billing/invoices/{INVOICE}") for line in invoice["lines"]: print(line["description"], line["amount_cents"], line["project_name"])

An invoice's status is pending, paid, failed, refunded or cancelled; its kind is initial, renewal, resize, admin_resize or trial_purchase. total_amount_cents includes tax (tax_amount_cents, tax_rate, tax_label). Every member of the organization can read its invoices; paying needs the Owner, Admin or Billing role.

Pay an invoice

Three ways to settle a pending invoice.

From the wallet. Instant, and it needs spendable_cents to cover the invoice:

curl

TerminalCode
curl -sS -X POST "$ZAROZ_API/billing/invoices/$INVOICE/pay-with-wallet" -H "$AUTH" \ -H "Idempotency-Key: $(uuidgen)" # {"entry_id": "..."}

TypeScript

TypeScriptCode
const paid = await call("POST", `/billing/invoices/${INVOICE}/pay-with-wallet`, undefined, { "Idempotency-Key": randomUUID(), }); if (paid.status === 422) console.log("not enough spendable balance, or already paid");

Python

Code
status, paid = call("POST", f"/billing/invoices/{INVOICE}/pay-with-wallet", headers={"Idempotency-Key": str(uuid.uuid4())}) if status == 422: print("not enough spendable balance, or already paid")

A wallet that cannot cover the invoice is a 422 that names the balance and the amount needed (nothing was attempted). An invoice that is already paid is also 422. A card or PayPal payment that is already in progress answers 409; nothing was charged. Paying is the one call where a blind retry could cost real money twice, which is why the Idempotency-Key matters. An organization token needs organizations.wallet.spend.

By card (Mollie) or PayPal. Both return a hosted page to send the customer to:

TerminalCode
curl -sS -X POST "$ZAROZ_API/billing/invoices/$INVOICE/checkout/mollie" -H "$AUTH" curl -sS -X POST "$ZAROZ_API/billing/invoices/$INVOICE/checkout/paypal" -H "$AUTH" # {"checkout_url": "https://..."}
TypeScriptCode
const { body } = await call("POST", `/billing/invoices/${INVOICE}/checkout/paypal`); console.log("Open", body.checkout_url);
Code
_, body = call("POST", f"/billing/invoices/{INVOICE}/checkout/paypal") print("Open", body["checkout_url"])

When the customer finishes, the provider sends them back to the dashboard; the invoice becomes paid when the provider confirms. Poll GET /billing/invoices/{id} rather than trusting the redirect. Check what the deployment offers (mollie, paypal) first: a provider that is not configured answers 501 capability-not-available.

Autopay

By default the wallet pays your renewal invoices automatically when it holds enough. Turn it off if you would rather approve each payment:

curl

TerminalCode
curl -sS "$ZAROZ_API/me/wallet/preferences" -H "$AUTH" # {"autopay_enabled": true, "preferred_currency": "EUR"} curl -sS -X PATCH "$ZAROZ_API/me/wallet/preferences" -H "$AUTH" -H "Content-Type: application/json" \ -d '{"autopay_enabled": false}'

TypeScript

TypeScriptCode
await call("PATCH", "/me/wallet/preferences", { autopay_enabled: false });

Python

Code
call("PATCH", "/me/wallet/preferences", {"autopay_enabled": False})

Switching autopay on authorizes the platform to spend the wallet on every renewal, so it needs the spending authority (Owner or Admin), not just the right to top up.

If you do not pay

An overdue invoice does not disappear. Your organization's standing reflects it and can restrict new purchases; GET /organizations/{id}/standing tells you whether you are restricted and, for owners, why.

Last modified on