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
WebhookEndpointyour business has subscribed tocheckout.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.
xOrderIdnever becomesdocument.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.

