# 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](/concepts/billing). This page is the money side, call by call. The samples use the helper from [Using the Zaroz API](/guides/api/overview). 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**

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

```json
{
  "balances": [
    { "currency": "EUR", "amount_cents": 3000, "real_cents": 2500, "promo_cents": 500,
      "legacy_cents": 0, "spendable_cents": 2500 }
  ]
}
```

**TypeScript**

```ts
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**

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

| Field | Meaning |
|---|---|
| `spendable_cents` | **What a wallet payment can use right now.** Use this to decide whether an invoice can be paid, not `amount_cents` |
| `real_cents` | Cash-like money: top-ups, refunds settled to the wallet |
| `promo_cents` | **Promotional 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_cents` | Older money not yet classified; not spendable by a wallet payment |
| `amount_cents` | All 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**

```bash
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**

```ts
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**

```python
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**

```bash
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**

```ts
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**

```python
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**

```bash
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**

```ts
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**

```python
_, 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**

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

**TypeScript**

```ts
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**

```python
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:

```bash
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://..."}
```

```ts
const { body } = await call("POST", `/billing/invoices/${INVOICE}/checkout/paypal`);
console.log("Open", body.checkout_url);
```

```python
_, 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](/guides/api/overview#what-a-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**

```bash
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**

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

**Python**

```python
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](/concepts/trust) reflects it and can restrict new purchases; `GET /organizations/{id}/standing` tells you whether you are restricted and, for owners, why.
