VUZDevelopers
Concepts

Payment Pages

One reusable page, a different transaction per sale — create it, sell against it, get notified.

Status: built and code-reviewed on the feat/partners-and-api branch, verified locally (build/lint/tests + live curl against a local instance). Not yet deployed to api.vuz.co.il — the tables and routes described here ship in a single deploy batch. Check with developers@vuz.co.il before wiring production traffic against this guide.

A Payment Page is a reusable, hosted checkout that a business creates once — a URL, a set of items or an open amount, branding, and optional server callbacks. What makes it an API primitive rather than just a UI feature: every visit or API call mints its own independent sale with its own cart, reference, and metadata. One page, unlimited distinct transactions — the same model iCount calls paypage/generate_sale.

The lifecycle, end to end

Create the page (once)

POST /api/v1/payment-pages (X-Api-Key, scope payments:write) — name, mode (one_time_fixed / one_time_flexible / recurring), optional default items, branding, and the two integration fields that matter most for API callers:

  • ipnUrl — where VUZ POSTs a signed notification after a successful sale on this page.
  • metadata — a small key/value object (≤ 12 keys, ≤ 40-char keys, ≤ 500-char values, ≤ 8 KB total) that becomes the default metadata merged into every sale from this page.

The response includes webhookSecret — shown once. Store it; it's how you verify the HMAC signature on ipnUrl deliveries (same scheme as Signature verification, just keyed with this per-page secret instead of a registered endpoint's secret). Lost it? POST /payment-pages/{pageId}/rotate-webhook-secret mints a new one — the old one stops working immediately.

Generate a sale (every visit / every API call)

curl -X POST https://api.vuz.co.il/api/v1/payment-pages/{pageId}/sales \
  -H "X-Api-Key: vuz_..." \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      { "description": "אגרת ביטול תיק 4412-07-26", "unitPrice": 250.00, "quantity": 1 }
    ],
    "xOrderId": "case-4412-07-26",
    "metadata": { "caseId": "4412", "action": "remove_case" }
  }'

Returns { "saleUniqId": "chk_9f2a...", "saleUrl": "https://pay.vuz.co.il/checkout/chk_9f2a..." }. Redirect your customer to saleUrl — that's the whole handoff. xOrderId is your own reference (order id, case number, invoice number — whatever you use to find this sale again later); metadata merges over the page's own default metadata, your keys win on collision.

Omit items entirely and the page falls back to its own configured default cart — that's what happens automatically when a customer opens the plain page URL (https://pay.vuz.co.il/p/{slug}) instead of you calling generate_sale yourself.

Customer pays

VUZ hosts the entire card-capture UI (Tranzila Hosted Fields — PAN/CVV never touch your servers or VUZ's). You don't build or embed anything for this step.

Get notified — checkout.paid

The moment the charge clears and the tax document is finalized, VUZ delivers a signed checkout.paid event to two places at once, additively:

  • Your page's ipnUrl, if set — this is the reliable, dedicated channel for this one page.
  • Any registered WebhookEndpoint your business has subscribed to checkout.paid (see Events catalog) — useful if you want one central webhook consumer across all your pages, not just this one.

Both deliveries are signed, logged, and retried on failure exactly like every other VUZ webhook (see Delivery & retries) — there is no separate, unsigned "IPN" mechanism to worry about; it's the same pipeline.

{
  "event": "checkout.paid",
  "id": "evt_01J9ZK...",              // stable across retries — dedupe on this, not on timing
  "createdAt": "2026-07-06T14:22:31Z",
  "environment": "live",               // or "sandbox"
  "checkout": {
    "id": "chk_9f2a...", "xOrderId": "case-4412-07-26",
    "amount": "250.00", "currency": "ILS",
    "ccLast4": "4242", "confirmationCode": "0071234"
  },
  "document": {
    "id": "...", "type": "tax_invoice_receipt", "number": "60012",
    "issueDate": "2026-07-06", "total": "250.00", "vatTotal": "38.46",
    "status": "issued"
  },
  "client": { "name": "ישראל ישראלי", "email": "...", "phone": "..." },
  "metadata": { "caseId": "4412", "action": "remove_case" }
}

Verify the signature, dedupe on id, then act — see Signature verification for the exact recipe in four languages. Only perform a destructive action (cancel an order, remove a record, grant access) after the signature check and the dedupe check both pass.

Belt and braces — reconciliation

Webhooks are push and can, in rare cases, be delayed by retries. If your integration performs an irreversible action on checkout.paid, add a periodic pull as a backstop:

GET /api/v1/payment-pages/sales?xOrderId=case-4412-07-26

(scope payments:read) — returns the checkout's current status, linked document, and metadata. A daily cron that checks "did every xOrderId I created a sale for either get paid or expire, and if paid, did I act on it" closes the loop even if a webhook delivery is ever missed.

document.finalized also fires

A payment-page sale is a first-class document issuance — any existing automation you've built on document.finalized (see Events catalog) fires for payment-page sales too, with the same enriched document + metadata payload. You don't need a separate integration for "email me every document" style rules just because the document came from a payment page instead of the regular documents API.

Sandbox testing

Real Tranzila Hosted Fields can't run outside production terminals. In VUZ's sandbox environment, the checkout flow accepts a simulated charge instead of a real one — the init step returns sandbox: true, and a magic-number card flow (documented in your sandbox onboarding) drives approve/decline outcomes without moving real money. Everything downstream of that point — document issuance, checkout.paid, webhook delivery — runs for real inside the sandbox, so you can build and test your entire integration, including the webhook consumer, before your first live sale.

What's deliberately NOT built

  • No coupon/discount engine. Model a discount as a negative-amount line item in the cart you send to generate_sale.
  • No file uploads on the payer form. Deferred — a public, unauthenticated upload surface needs its own security review before it ships.
  • xOrderId never becomes document.externalReference. That field is reserved for software-import provenance (e.g. the WooCommerce plugin). The durable link between your reference and the resulting document is the checkout row itself — use the reconciliation endpoint above, not the document's own fields, to look a sale back up by your reference.

On this page