Skip to main content

Agent quickstart — Element Pay Partner API

Short path for coding agents and humans. Contract: ../openapi.yaml. Standard local fiat ↔ USDC/USDT: integration-fiat-stablecoin.md. Gaps: KNOWN_GAPS.md.

Install / auth

No SDK required. Call HTTPS JSON from your backend.
Every request:
Request access: email compliance@elementpay.net (company, corridors, sandbox webhook_url). See sandbox/onboarding.mdx. Security scheme in OpenAPI: APIKeyHeader (X-API-Key). Webhook HMAC uses webhook_secret on the key — not request auth.

Sandbox credentials / test amounts

Never send sandbox phones/1111111111 accounts on production. Full matrix: sandbox/success-failure.mdx.

Happy path — local fiat → USDC (OnRamp)

Same endpoints for every corridor: discover → quote → accept → order.settled. Only country / currency / network_id / amount / asset change. OffRamp (crypto → local) is documented in integration-fiat-stablecoin.md. Worked example below: KE M-PESA OnRamp → Base USDC (fund portal: credit ledger on settle).

Numbered steps (copy-pasteable)

0. Env
1. KYC (required) — every end-customer needs vault customer_id (pcus_*) with status=approved before quote. Full curls: customers/quickstart.mdx. Minimal flow:
  1. GET /partner/customers/requirements
  2. POST /partner/customers → save pcus_*
  3. POST /partner/customers/{customer_id}/documents (identity + address)
  4. POST /partner/customers/{customer_id}/submit
  5. Wait for customer.approved webhook or poll GET /partner/customers/{customer_id} until status is approved
2. Discover M-PESA network_id (do not hardcode across envs)
3. Quote — OpenAPI: POST /partner/orders/quote
4. Accept (wait ~2s; OpenAPI: POST /partner/orders/{quote_id}/accept)
5. Settle — prefer webhook order.settled; backup poll:
Then credit your fund-portal ledger with amount_crypto (API returns decimal stablecoin; store as integer minor units × 10⁶ if that is your ledger convention). Details: integration-fiat-stablecoin.md.

Webhook verify + idempotency

Headers (every delivery):
Verify (from webhooks.mdx):
  1. Parse t=…,v1=…
  2. Reject if t older than 5 minutes
  3. HMAC-SHA256 over {t}.{raw_body} with YOUR_WEBHOOK_SECRET; constant-time compare to v1
  4. Dedupe on X-Webhook-Id; return 2xx quickly
Order events: order.processingorder.settled | order.failed | order.refunded. Idempotency elsewhere Corridor quotes do not take an idempotency_key on quote/accept.

Banking (optional) — customers + accounts

Corridor ramps need vault KYC only (status=approved). Accounts are a separate product: EUR/USD IBAN, stablecoin ledger, deposit instructions, sends, book transfers, conversions, payouts. Full map: customers/route-cheat-sheet.mdx · guide: customers/accounts.mdx. approved alone does not unlock accounts. Calling account routes before ready409 (Deposit account is not ready). Prefer webhook customer.deposit_account.updated; poll GET /partner/customers/{customer_id} as backup.

Sequence

  1. Wait until products.deposit_account.status is ready
  2. POST /partner/customers/{customer_id}/accounts (open fiat or stablecoin rail; idempotent on rail tuple)
  3. GET /partner/customers/{customer_id}/accounts → save account_id
  4. GET …/accounts/{account_id}/deposit-instructions when you need bank details or a deposit wallet
  5. Money movement (each has its own guide; confirm uses idempotency_key):
    • Stablecoin sends: …/sends/preview…/sends
    • Book transfers (same currency): …/book-transfers/preview…/book-transfers
    • Conversions (cross-currency, same pcus_*): …/conversions/preview…/conversions
    • External bank payouts: …/payout-methods…/payout-banks…/payouts/preview…/payouts

Open examples (sandbox)

Notes from existing guides (do not invent beyond these):
  • Sandbox outbound sends/conversions/payouts may need an Element Pay sandbox credit (customers/deposit-instructions.mdx).
  • USD fiat open is not available for Kenya individuals (unsupported_region) — see customers/accounts.mdx.
  • Card routes are documented in customers/cards.mdx but not in openapi.yaml yet (docs/KNOWN_GAPS.md).
Account webhooks: account.opened, account.ready, account.credited, account.send.completed, account.send.failed.

Common pitfalls

  1. Quoting before vault status=approved422
  2. Calling accounts before deposit_account.status=ready409
  3. Hardcoding network_id from docs instead of your catalog
  4. Using sandbox success phones on production
  5. Treating GET /partner/rates/indicative as the binding FX (it is not; use quote amounts.rate)
  6. Confusing corridor OnRamp with ledger …/conversions (fiat↔fiat on banking accounts only)
  7. Crediting the user ledger on accept/order.processing instead of order.settled
  8. Missing webhook signature check or clock skew / replay beyond 5 minutes
  9. Reusing OffRamp crypto deposit addresses across orders (per-order in production)

OpenAPI path map

Ramp happy path

Banking (after deposit_account.status=ready)

All listed under paths in openapi.yaml (except cards — see KNOWN_GAPS.md).