# Purchases via the API — balance payment and a top-up link

> How an AI agent orders SSL, registers and renews domains with an API key: payment from the balance, a top-up link for a person, payment notice.

URL: https://zrazu.com/en/docs/api-orders/

A script or AI agent can buy with an API key, but it pays only from the organization's balance. If the balance is short, the API does not refuse — it returns a link for a person: they top up exactly the shortfall by card or by an IBAN invoice, and the order goes to work at once. The agent learns about the payment by itself and carries on.

Right now this works for ordering an SSL certificate (the `ssl.order` right) renewing a domain (the `domain.renew` right) and registering a new one (the `domain.register` right), see [API keys](https://zrazu.com/en/docs/api-keys.md). An agent can do the same through [MCP with a key](https://zrazu.com/en/docs/api-mcp-account.md).

## How it works

1. The agent orders a certificate: `POST /v1/ssl/certificates` with `"if_short": "pay_link"`.
2. If the balance covers it, the amount is charged at once. The answer is `201`, the certificate goes to domain validation.
3. If money is short, nothing is charged or reserved. The answer is `402` with the code `payment_required` and a `payment_url` link.
4. The agent passes the link to a person: in a chat, by email, in a message.
5. The person opens the link without signing in, sees the order and the amount due, and pays by card or by an IBAN invoice.
6. The money lands on the balance, and the order is paid from the balance right away.
7. The agent sees the order is paid (via `GET /v1/orders/{id}` or a webhook) and goes on: waits for issuance and fetches the certificate files.

Without `if_short` the API behaves as before: when money is short it answers `402 balance_short` and creates no order.

## An order with a top-up link

```sh
curl -X POST https://api.zrazu.com/v1/ssl/certificates \
  -H "Authorization: Bearer zr_live_..." \
  -H "Idempotency-Key: 0b7c6e1a-3f42-4d8e-9a51-2c6f0e9d7b10" \
  -H "Content-Type: application/json" \
  -d '{"product_id": "…", "mode": "vat", "domain": "example.com.ua",
       "csr": "-----BEGIN CERTIFICATE REQUEST-----…", "dcv_method": "dns",
       "if_short": "pay_link"}'
```

The answer when money is short:

```json
{
  "error": { "code": "payment_required", "message": { "uk": "…", "en": "…" } },
  "amount_due": 22500,
  "payment_url": "https://vse.zrazu.com/pay-link/…/",
  "expires_at": "2026-11-16T12:00:00Z",
  "order": { "id": "…", "number": "202611-48271", "status": "awaiting_payment", "amount_due": 22500, "items": [ … ] },
  "certificate": { "id": "…", "status": "unpaid", … }
}
```

Amounts are in kopecks. `amount_due` is the order minus the live money on the balance; bonuses are not counted.

## The top-up link

- The link is valid for 72 hours. The page shows the order, the amount due and the seller. Your organization's data is not on it.
- By card: the bank fee is added on top, and the button shows the full amount. By IBAN invoice: the invoice is issued for the shortfall, and its number matches the order number.
- The amount is recalculated every time the page is opened. If the balance was topped up meanwhile, less is due.
- If the link was lost or has expired, `POST /v1/orders/{id}/payment-link` issues a new one. The previous link of this order then stops working. While a person is paying by card, no new link is issued: the answer is `409 payment_in_progress`.
- If the balance was topped up another way (in the cabinet, by transfer), the agent pays the order itself: `POST /v1/orders/{id}/pay`. If money is short again, the answer is `402`. While the previous link is valid there is no new one (`payment_url` is `null`): the person pays through the same link.
- A key can pay for and issue links only for orders it made itself. An order from the cabinet or another key does not exist for it (`404`).

The link is the right to top up exactly this order. Pass it to people you trust: the amount due hints at roughly how much is on the balance.

## How the agent learns that the payment arrived

**Polling is the main way for AI agents.** Chat assistants do not accept incoming requests, so they ask:

```sh
curl -H "Authorization: Bearer zr_live_..." "https://api.zrazu.com/v1/orders/{id}?wait=60"
```

With `wait` (up to 60 seconds) the answer comes as soon as the order changes, or after `wait` seconds if nothing happened. Repeat the call in a loop. Up to 3 waits at once per key and up to 10 per organization; beyond that you get `429 too_many_waits`, so ask without `wait`.

| Field | Meaning |
| --- | --- |
| `status` | `awaiting_payment` — waiting for money; `review` — money arrived, the order is in a security review; `paid` — paid; `partly_failed` — part was not fulfilled, money for it is on the balance; `cancelled` — cancelled |
| `amount_due` | how much is still due, in kopecks. The amount reveals the balance, so a key with only `ssl.read` gets `null`; keys with `ssl.order` or `billing.read` see it |
| `fulfilled` | `true` — everything is done: the certificate is issued or finally refused |
| `items[].certificate_id` | the certificate id: the agent continues with `/v1/ssl/certificates/{id}` |
| `payment_link_expires_at` | until when the latest link is valid; `null` — no link or it has expired |

A key with the `ssl.read`, `ssl.order` or `billing.read` right can read orders.

**A webhook is for server integrations.** If the organization has a webhook, ZRAZU sends the events `order.paid`, `order.fulfilled` and `order.cancelled` for orders that got a link. In the request body, the `data` field holds `event_id` and the order in the same shape as `GET /v1/orders/{id}`. The request is signed like other webhooks (`X-Zrazu-Signature`). If your server did not answer, ZRAZU repeats the event with a pause that grows up to an hour. So an event may arrive twice: tell repeats apart by `event_id`. Events are not sent if the webhook has the «Money» category turned off.

## Domain renewal

The key needs the `domain.renew` right. A renewal is a separate order and does not go into the cabinet cart.

```sh
curl -X POST https://api.zrazu.com/v1/domains/example.com.ua/renew \
  -H "Authorization: Bearer zr_live_..." \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"years": 1, "expires_at": "2027-03-14", "if_short": "pay_link"}'
```

- `years` — from 1 to 10. After renewal the domain term cannot reach further than 9 years from today.
- `expires_at` — required: the current expiry date from `GET /v1/domains`. If ours already differs, the domain was renewed earlier and the API answers `409 expiry_mismatch`: you will not pay twice. The field is optional, but we recommend sending it.
- `mode` — `vat` or `no_vat`. Needed only if the domain has not been paid with us yet (for example, it was just transferred in). Otherwise the same seller as last time renews it.
- If there is enough money, the answer is `201` with the order in `paid` status. The new term shows in `GET /v1/domains` as soon as the registry confirms the renewal; `fulfilled` in the order becomes `true`.
- If money is short, it works as for SSL: without `if_short` — `402 balance_short` and no order; with `"if_short": "pay_link"` — `402` with a link.

## Domain registration

The key needs the `domain.register` right. A registration is a separate order and does not go into the cabinet cart.

1. Take the domain owner from `GET /v1/contacts/owners`: `id`, kind (`person` or `org`), name, `complete` (whether a full postal address is there) and `authority` (whether authority for this person is confirmed in the cabinet). There is no email or phone. The key does not create owners: a person adds a record in the cabinet, under "Contacts". The key cannot change the owner later, so choose carefully.
2. Send the request without `documents_hash`. If the registry requires consent to its documents, the API answers `409 documents_required` with `documents` (title, edition, link) and `documents_hash`.
3. Read the documents and repeat the request with that `documents_hash`. The repeat with the hash is your organization's consent to the registry documents in the editions shown. If the domain owner is another person, you consent on their behalf and warrant that you have the authority to do so and that they have read these documents. If the documents changed, you get `documents_required` again with a new hash.

```sh
curl -X POST https://api.zrazu.com/v1/domains/example.com.ua/register \
  -H "Authorization: Bearer zr_live_..." \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"years": 1, "owner": "RECORD_ID", "mode": "no_vat", "documents_hash": "HASH_FROM_THE_ANSWER", "if_short": "pay_link"}'
```

- `years` — from 1 to 10.
- `owner` — required: a record `id` from `GET /v1/contacts/owners`.
- `mode` — required: `vat` or `no_vat`.
- Until the right `documents_hash` is sent, no order is created and no money is charged.
- If there is enough money, the answer is `201` with the order in `paid` status. The domain shows in `GET /v1/domains` as soon as the registry registers it; `fulfilled` in the order becomes `true`.
- If money is short, it works as for SSL. The person on the top-up page sees the same documents and agrees to them personally.

## Limits

- The key's daily spending limit also counts orders waiting for payment.
- Before payment the order passes the same security check as a payment in the cabinet. If it needs a manual check, the status is `review`.
- Paying a registration order with `POST /v1/orders/{id}/pay` checks the owner and the documents again. If the owner record was removed from the directory or the registry documents changed, payment is refused: place the order again.
- An unpaid order stays in the cabinet under «Orders». It can be paid or cancelled there.

## If it did not work

- **`409 documents_required`.** Read `documents` or show them to a person and repeat the request with `documents_hash` from this answer: the repeat means consent.
- **`422 owner_required`, `owner_not_found`.** Pass `owner` — an `id` from `GET /v1/contacts/owners`.
- **`422 owner_unconfirmed`.** Authority is not confirmed for the record: open it under "Contacts", tick the box and save.
- **`422 owner_incomplete`.** The owner has no full postal address: complete it in the cabinet or pick another record.
- **`409 name_taken`.** The name is already registered.
- **`409 register_unavailable`.** The registry does not answer or registration in the zone is paused. Try later.
- **`429 rate_limited`.** The key, organization or address used up its share of name checks at the registry. Retry in a minute.
- **`409 expiry_mismatch`.** The domain was renewed already: check the term in `GET /v1/domains`.
- **`422 mode_required`.** The domain has not been paid with us yet: pass `"mode": "vat"` or `"no_vat"`.
- **`409 order_refused`.** The renewal was not accepted: the domain is already being renewed, renewal is stopped, or the term would exceed 9 years. The reason is in `message.uk`.
- **`409 renew_unavailable`.** The domain cannot be renewed right now: it is locked, a registry procedure is still running, or the registry does not answer. Try later.
- **`402 balance_short`.** The request has no `"if_short": "pay_link"`: add the field to get a link.
- **`409 not_awaiting_payment`.** The order is already paid or cancelled. Check `GET /v1/orders/{id}`.
- **`409 balance_enough`.** The balance already covers it. Pay the order: `POST /v1/orders/{id}/pay`.
- **`409 payment_in_progress`.** A person is paying by card right now. Wait for the result with `GET /v1/orders/{id}?wait=60`.
- **The page says the link is not valid.** The address was copied incompletely. Ask the agent to send the link again.
- **The page says the link has expired.** It ran out of time or a newer link was issued for the order. Ask the agent to send the latest one or issue a new one: `POST /v1/orders/{id}/payment-link`.
