ZyloPaydocs

Payment intents

A payment intent asks one terminal to take one payment. How to create, follow and cancel it.

A payment intent (pi_…) is one attempt to take one amount on one terminal. Use it for server-driven checkout: your ordering or POS system decides the amount, and the customer taps at the counter.

Create

const intent = await zp.paymentIntents.create(
  {
    amount: 2500000,          // 2.50 USDC, in atoms
    terminal: 'tml_ZP-4821-K',
    order_id: 'A7K2Q9XZ',     // optional, 1-8 characters, written on-chain
    description: 'Table 12',  // shown on the terminal
    metadata: { pos_order: '48213' },
    expires_in: 120,          // seconds, 30-600
  },
  { idempotencyKey: `intent-${posOrderId}` },
);
FieldRules
amountInteger USDC atoms, at least 1. The customer pays the network fees on top.
terminalAn active terminal of the same mode (tml_… from GET /v1/terminals). A deactivated terminal answers 409 terminal_inactive.
order_idOptional. 1-8 characters of A-Z a-z 0-9 - _. It is written on-chain with the settlement, so it must be unique per account and mode and must not have been paid before. Generated when omitted.
descriptionOptional, up to 500 characters. Shown on the terminal while it waits.
metadataOptional. Up to 20 string pairs, returned unchanged. Never put secrets or personal data here.
expires_inOptional, 30-600 seconds, default 120.

Idempotency-Key is required. Derive it from your own order so a crash-and-retry of your process finds the same intent instead of creating a second one. See Idempotency.

Live mode also checks that your business verification is approved (403 kyb_required) and that your account can take payments (403 account_restricted or account_disabled).

How the terminal takes it

A terminal collects its API-created intents by polling ZyloPay with its own device key, oldest first. It shows the amount and description, reads the customer's pass over NFC, and settles through the same path as a payment the cashier starts: the same checks, limits, confirmation rules and receipts. The pass credential never reaches your servers.

ZyloPay POS app support

The terminal protocol is live. The ZyloPay POS app picks up intents from the release that adds intent pickup, which is rolling out now. Before you rely on server-created intents, confirm with support that your terminals run it; until then, use terminal-started payments. See Terminals.

Follow

Listen to the payment_intent.* events, or retrieve the intent. Reads never change its state.

const current = await zp.paymentIntents.retrieve(intent.id);
if (current.status === 'succeeded') await fulfil(current.order_id, current.payment);

payment is filled once the settlement is indexed, usually together with succeeded. If you need the payment object (fees, payer, block), use the payment.succeeded event or GET /v1/payments/{id}.

Cancel

await zp.paymentIntents.cancel(intent.id, { idempotencyKey: `cancel-${intent.id}` });
  • You can cancel while the intent is requires_payment_method or requires_action. A pending payer confirmation is declined first.
  • If the settlement was already sent, or the customer already confirmed, the cancel answers 409 payment_intent_unexpected_state. Refund the payment instead.
  • Canceling a canceled intent returns it unchanged.

Expiry

An intent that nobody pays is canceled at expires_at with cancellation_reason: "expired", and payment_intent.canceled is sent. Reading an expired intent already shows canceled.

Intents the terminal starts

When a cashier enters an amount on the terminal, ZyloPay creates an intent too, with source: "terminal". It follows the same statuses and sends the same events, except payment_intent.created, which is sent only for intents your server creates. Filter with GET /v1/payment_intents?source=api or ?source=terminal.

On this page