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.jsonon 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)
- 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.
- 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. - Never call a live payment API while developing. Use the offline kits and mocks below.
- 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
UNKNOWNand ask the owner. - No database migration or destructive change without separate human approval. If your design needs one, stop at that boundary and report the exact prerequisite.
- Do not commit, push or deploy unless asked.
1. Machine-readable entry points
| What | Where |
|---|---|
| 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 pages | Cashier 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 side | Eleven Pay field | Rule |
|---|---|---|
| order id | merchant_order_id | 1–100 chars A-Za-z0-9_.:-; stable; one invoice per order |
| payable total | amount | string, two decimals, whole tenge ("1500.00"); take it from the database, never the browser; refuse (do not round) fractional totals |
| currency | currency | only "KZT" |
| payer phone | customer_phone | 10–15 digits; strip only spaces, dashes, parentheses and one leading +; do not "fix" the number (canonical national form is UNKNOWN) |
| retry identity | Idempotency-Key header | 1–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 reference | store with the order | ep1_…; 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_statemovesqueued → sending → sentin 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:
- Read the raw request bytes (no body parser in front of the verifier).
- Header
X-ElevenPay-Signature: t=<unix>,v1=<hex>[,v1=<hex>…]— accept if anyv1equalshex(HMAC_SHA256(secret_utf8, "<t>." + raw_body)); compare in constant time; reject if|now − t| > 300seconds; ignore unknown schemes (v2=…). - Only then parse JSON. Handle
type == "payment.succeeded"withdata.payment.status == "succeeded"; ignore other types with2xx. - Deduplicate on
X-ElevenPay-Event-Id/ bodyid. - Match order, amount and currency to your database; mismatch → manual review, no fulfilment.
- 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. - Return
2xxonly 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_uniquefor 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 withGET, escalate if it persists.409 ambiguous_create_blockedmeans 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)
- Unpack the kit; run its tests:
python -m unittest discover -s python -p "test_*.py"andcd node && node --test. - Run
python offline_demo.py/node offline-demo.mjsfor the full story on mock services. - Reuse
test-vectors/lifecycle-vectors.json(17 scenarios) andtest-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. - 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
| Create | POST /v1/cashier/invoices (capability cashier.invoice.create, key env live) |
| Read | GET /v1/cashier/invoices/{reference} (capability payment.read; unknown/foreign → 404 invoice_not_found) |
invoice_state | queued · sending · sent · not_sent · unknown |
payer_action | null · "approve_in_kaspi_app" |
status | shared enum (pending, processing, succeeded, cancelled, failed, outcome_unknown); invoices currently reach pending → succeeded |
| Events | payment.succeeded (the only event produced for cashier invoices) |
| Webhook headers | X-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 |