Skip to content

AI agent integration guide ​

Audience: an AI coding agent (or a developer) integrating a merchant website with Eleven Pay without access to the Eleven Pay source repository. Language of this page: English (identifiers are exact). Russian/Kazakh onboarding: Быстрый старт, Бастау.

Pilot status — read first

  • Eleven Pay is a pilot. Real payment acceptance through cashier invoices is not yet available; there is no sandbox with real money. Everything you build is validated offline with mocks.
  • The public production API host is not decided. Never hard-code or invent one. The base URL is provided to the merchant at onboarding and must be a configurable value (ELEVENPAY_BASE_URL).
  • This documentation is served from docs.eleven.kz; the machine-readable contract is /openapi.json on the same origin. That origin is a static documentation site: it is not the API and accepts no calls.
  • Eleven Pay is an independent service. It is not operated by, endorsed by or affiliated with Kaspi.kz or any bank; names of third-party services appear only to describe what the API does.

0. Ground rules (hard constraints) ​

  1. Do not change, disable or replace the merchant's existing live payment integration unless a human owner explicitly approves that exact change. Add the new path behind a feature flag that is OFF by default.
  2. Never read, print, log or commit secrets (.env, API keys, webhook secrets, tokens, passwords, service-account files). Refer to them by variable name only. Never put a real key in a test.
  3. Never call a live payment API while developing. Use the offline kits and mocks below.
  4. Do not guess repository paths, hosts, tenant identifiers or legal-entity mappings. If a fact is not in the merchant's repository or in this documentation, mark it UNKNOWN and ask the owner.
  5. No database migration or destructive change without separate human approval. If your design needs one, stop at that boundary and report the exact prerequisite.
  6. Do not commit, push or deploy unless asked.

1. Machine-readable entry points ​

WhatWhere
OpenAPI 3.1 contract (four public operations)/openapi.json
Agent index/llms.txt
Tested Python + Node.js kits, shared lifecycle vectors, cURL scripts/downloads/eleven-pay-merchant-kit.zip
Human pagesCashier invoices · Create · Status · Webhooks · Idempotency · Errors

The OpenAPI servers entry is the variable {api_base_url} with no real default. Fill it from configuration.

2. Inspect the merchant's real application (read-only) ​

Find these facts in the actual repository (do not assume the framework):

  • framework and runtime; language; package manager; test, lint and type-check commands;
  • database, ORM, and how migrations are applied;
  • authentication and user accounts (who may pay for what);
  • the order / purchase lifecycle: tables, statuses, who creates orders, where totals are computed (including discounts and promo codes — Eleven Pay requires a whole number of tenge);
  • the existing payment integration(s) and the exact code path that marks an order paid;
  • fulfilment: what happens after payment (unlock content, issue a certificate, grant credits) and whether it is already idempotent;
  • background jobs, cron, webhook routes, and existing environment-variable naming and validation;
  • i18n/UI rules and any CI checks (secret scans, text policy) your change must satisfy.

Write the findings down before designing. Mark every unverified item UNKNOWN.

3. Map the merchant's domain to Eleven Pay ​

Merchant sideEleven Pay fieldRule
order idmerchant_order_id1–100 chars A-Za-z0-9_.:-; stable; one invoice per order
payable totalamountstring, two decimals, whole tenge ("1500.00"); take it from the database, never the browser; refuse (do not round) fractional totals
currencycurrencyonly "KZT"
payer phonecustomer_phone10–15 digits; strip only spaces, dashes, parentheses and one leading +; do not "fix" the number (canonical national form is UNKNOWN)
retry identityIdempotency-Key header1–255 visible ASCII; stable per order: store it with the order before the first request, or derive it deterministically from the order id; never generate a new one on retry
returned referencestore with the orderep1_…; needed for GET reconciliation

State worth persisting per order (so a process restart can recover): idempotency key, reference (after the first response), last invoice_state, order deadline, processed webhook event ids.

4. Configure ​

Use environment variables read only on the server: ELEVENPAY_ENABLED (your feature flag, default off), ELEVENPAY_BASE_URL, ELEVENPAY_API_KEY, ELEVENPAY_WEBHOOK_SECRET. If any is missing or invalid the integration must stay off and the existing payment path must continue unchanged. Keys have the shape ep_<env>_<keyid>_<secret>; cashier invoices require a live key with capability cashier.invoice.create (+ payment.read for reads). Never expose these to a browser bundle.

5. Create the invoice ​

POST {ELEVENPAY_BASE_URL}/v1/cashier/invoices — headers Authorization: Bearer <key>, Idempotency-Key, Content-Type: application/json; body exactly {"amount":"1500.00","currency":"KZT","merchant_order_id":"…","customer_phone":"…"} (no extra fields). Response 201 (or 200 for a replay; header Idempotency-Replayed): reference, merchant_order_id, status, amount, currency, environment, created_at, invoice_state, payer_action, settled.

  • No provider call happens inside the request. The invoice is queued; invoice_state moves queued → sending → sent in the background.
  • Tell the payer to approve in their app only when payer_action == "approve_in_kaspi_app".
  • Verify the response matches your order (merchant_order_id, amount, currency) before trusting it.
  • One attempt = one request. Do not add blind retries. On transport failure the outcome is unknown: retry later with the same key and same body (the original invoice is returned).

6. Register the webhook ​

In the merchant cabinet (Webhooks), register an HTTPS URL before the first payment: domain host (no IP literal), default port, no credentials/query/fragment, no redirects. One enabled endpoint per organization. The signing secret is shown once; store it as ELEVENPAY_WEBHOOK_SECRET.

7. Verify settlement before fulfilment ​

Treat an order as paid only from a verified settlement:

  1. Read the raw request bytes (no body parser in front of the verifier).
  2. Header X-ElevenPay-Signature: t=<unix>,v1=<hex>[,v1=<hex>…] — accept if any v1 equals hex(HMAC_SHA256(secret_utf8, "<t>." + raw_body)); compare in constant time; reject if |now − t| > 300 seconds; ignore unknown schemes (v2=…).
  3. Only then parse JSON. Handle type == "payment.succeeded" with data.payment.status == "succeeded"; ignore other types with 2xx.
  4. Deduplicate on X-ElevenPay-Event-Id / body id.
  5. Match order, amount and currency to your database; mismatch → manual review, no fulfilment.
  6. In one transaction: record the event id, move the order with a conditional update (… WHERE status = 'awaiting_payment' / your equivalent) and enqueue fulfilment (outbox). Ship the goods from the outbox, not inside the handler.
  7. Return 2xx only after durable recording. Your own failure → 5xx (Eleven Pay retries).

Alternative/second source: GET /v1/cashier/invoices/{reference} → paid only if status == "succeeded"and settled == true. A 201, payer_action, the end of your polling, or the payer returning to your site is never proof of payment. Both sources must go through the same conditional update.

Delivery: at-least-once; up to 7 attempts after the delays 1 min, 5 min, 15 min, 1 h, 6 h, 24 h; 2xx stops retries, 408/429/5xx/network errors retry, other 4xx and 3xx are terminal; every attempt is re-signed with a fresh t.

8. Prevent duplicates ​

  • Duplicate invoices: stable idempotency key + one invoice per merchant_order_id (409 cashier_order_not_unique for a second attempt with a new key). Disable the pay button while a request is in flight and rely on the key for concurrency.
  • Duplicate fulfilment: event-id table + conditional status update + outbox with a unique row per order.
  • Same key, different request → 409 idempotency_conflict: a bug in your key scheme.

9. Uncertainty, expiry and late payment ​

  • invoice_state: "unknown" or no HTTP answer → do not create another invoice for the order; wait for the webhook, reconcile with GET, escalate if it persists. 409 ambiguous_create_blocked means the same.
  • invoice_state: "not_sent" → no payment exists; present an error and let the payer start a new order.
  • Eleven Pay does not expire or cancel an invoice (no cancel endpoint, no "expired"/"failed" event for invoices — behaviour for failure/expiry is UNKNOWN/undefined). A merchant-side deadline only changes your order; it cancels nothing remotely, and the invoice stays payable.
  • A verified payment for an order you already expired is a late payment: acknowledge with 2xx, record it for human review by default (refunds are unavailable for cashier invoices), never drop it, and never auto-fulfil unless the owner chose that policy.

10. Test locally with mocks (no network, no credentials) ​

  1. Unpack the kit; run its tests: python -m unittest discover -s python -p "test_*.py" and cd node && node --test.
  2. Run python offline_demo.py / node offline-demo.mjs for the full story on mock services.
  3. Reuse test-vectors/lifecycle-vectors.json (17 scenarios) and test-vectors/webhook-vectors.json (19 signature cases) in your own test-suite. Cover: success, duplicate request, duplicate webhook, invalid signature, ambiguous/failed creation, expired order, late settlement, restart recovery, amount/order mismatch, and no change to the existing payment flow when the flag is off.
  4. Run the merchant's own lint, type-check and test commands. Do not deploy.

11. Definition of done ​

  • Feature flag off by default; with it off, behaviour and tests of the existing payment flow are unchanged.
  • Webhook verifies signatures on raw bytes and fails closed; fulfilment is exactly-once under replays.
  • Amount/currency/order are checked against the database before fulfilment.
  • No secret in code, tests, logs or docs; base URL is configuration.
  • Report back: files changed, tests run (with counts), remaining UNKNOWNs, and the human approvals still required before any live use.

Exact contract at a glance ​

CreatePOST /v1/cashier/invoices (capability cashier.invoice.create, key env live)
ReadGET /v1/cashier/invoices/{reference} (capability payment.read; unknown/foreign → 404 invoice_not_found)
invoice_statequeued · sending · sent · not_sent · unknown
payer_actionnull · "approve_in_kaspi_app"
statusshared enum (pending, processing, succeeded, cancelled, failed, outcome_unknown); invoices currently reach pending → succeeded
Eventspayment.succeeded (the only event produced for cashier invoices)
Webhook headersX-ElevenPay-Signature, X-ElevenPay-Event-Id, X-ElevenPay-Event-Type, X-ElevenPay-Delivery-Attempt, X-ElevenPay-Environment, User-Agent: ElevenPay-Webhook/1
Errors{"error":{"code","message"}} — see Errors and Create