Skip to main content
This page is a dense, fact-first brief for AI coding assistants (Claude, Cursor, Copilot, Cody, etc.) helping a developer integrate Kira’s API. It is optimized for fast, correct integration — not human reading.
What this page is. A canonical brief you can give to your AI coding assistant. Save it to your repo root as AGENTS.md (Claude Code and most AI tools auto-detect this filename), or paste it into your Claude / Cursor / ChatGPT system prompt with the header “Use this brief when writing code that calls Kira’s API.” Then tell your AI: “Build me a [thing] that talks to Kira. Use this brief as your source of truth — don’t guess.” The agent will then have everything it needs to integrate without round-tripping with you for every field name.

Feed this brief to your agent

These docs are auto-published in machine-readable forms — always current, so your agent never works from a stale paste. Prefer fetching one of these over copying the text by hand:

Whole-API context

docs.kirafin.ai/llms-full.txt — every page on this site (this brief, the guides, and the full API reference) concatenated into one file.

Just this brief

docs.kirafin.ai/guides/for-ai-agents.md — the raw Markdown of this page. Every page is fetchable at its path plus .md.

Index of all pages

docs.kirafin.ai/llms.txt — a link index your agent can crawl to pull only the pages it needs.
You can still save this page as AGENTS.md in your repo (per the note above) — but pointing your agent at a URL means it always reads the latest facts.

Connect the docs MCP server

These docs are also served as a hosted MCP server at https://docs.kirafin.ai/mcp. Connect your agent to it and it can search and read the docs — including the full API reference — on demand, instead of relying on a one-time paste.
Every page here carries a contextual menu in its header — use it to copy the page as Markdown, open it in Claude / ChatGPT, one-click install this MCP server in Cursor or VS Code, or download the OpenAPI spec.

Identity & versioning

  • Sandbox base URL: https://api.balampay.com/sandbox
  • Production base URL: https://api.balampay.com (do not call until production credentials are issued)
  • API version: 2026-04-14 (always send X-Api-Version: 2026-04-14 header until the account is pinned)
  • Reference data: GET /v1/countries returns {count:250, data:[...]}. (/api/countries and /countries return 403 — wrong path.)
  • Credentials needed from Kira: api_key, client_id (UUID), password. Provided by your Kira contact through a secured channel.

Auth flow

Call POST /auth with only your x-api-key:
Returns:
Cache the token, refresh when it’s less than 5 min from expiry. Re-auth on any 401 with retry.
See Authentication for the full flow.

One-time setup: pin the account to v2026-04-14

The body field is target_version, not version. After this, the X-Api-Version header becomes optional. See Versioning.

Required headers — every mutating request

Update users with PUT /v1/users/{id} (→ 200), NOT PATCH. PATCH is not supported on that route. A wrong path/method returns a clean routing error — 403 invalid_request on a normal authenticated call (404 route_not_found if unauthenticated) — not an auth or signature failure. See Known limitations.
Endpoints requiring Idempotency-Key:
  • POST /v1/users
  • POST /v1/recipients
  • POST /v1/virtual-accounts
  • POST /v1/virtual-accounts/{id}/payout
Rule: generate a NEW UUID v4 for every distinct logical request. Reuse the SAME key only for retries of the same logical request.

Core resources & their state machines

For the full state-machine reference, see State machines; for the webhook catalog, see Webhooks.

User (will be renamed Client in v2026-XX-XX)

VERIFIED is the KYC gate only — it does NOT mean the user is product-ready. A VERIFIED user is normally eligible: false per product until that product’s required fields are filled. Read GET /v1/users/{id}eligible_products[] (product_code, eligible) and the top-level missing_fields map (product_code → [field tokens]).
Timing: the missing_fields map is computed from your payload and available immediately — on the 201 response and on GET /v1/users/{id}. Per-product eligible flips once verification completes. Final approval is a manual step, not instant — per Kira it can take up to 24 hours — and the sandbox can route a complete user to manual review (status: REVIEW) at any point in that window, which resolves on Kira’s side. Don’t build against a “verifies in seconds” assumption.
Fill scalar gaps with PUT /v1/users/{id}, document gaps by re-submitting uploads, then the product flips eligible: true. ACT (usa-virtual-accounts-act) and Portage (usa-virtual-accounts) have different field requirements. Conditional tokens you’ll see: ssn:unless_immigration_status:non_us_citizen, occupation:when_employment_status:employed, identifying_information:back:unless_doc_type:passport, plus associated_persons: / additional_info: prefixes. For businesses, a beneficial owner is recognized by has_ownership (true/false), not by role — set it on every associated_persons[] entry (an omitted flag surfaces as associated_persons:has_ownership in missing_fields; advisory, non-blocking). To get a user to VERIFIED in sandbox: create a COMPLETE user (all product fields + document uploads present and well-formed — scalar values may be fake, but document file values must be real retrievable bytes: a placeholder or unreachable URL blocks the provider submission and the user stays CREATED). Verification triggers automatically, but approval is manual, not instant — per Kira it can take up to 24 hours; don’t build against a “verifies in seconds” assumption. It can land in manual review (status: REVIEW, verification_status: in_review), which a Kira operator resolves — more likely for businesses (KYB) and low-quality/placeholder document images. Verification only triggers once the user is complete (verification_triggered: true); an incomplete user is NOT rejected — it stays CREATED with verification_triggered: false and the gaps listed in missing_fields (fill them with PUT /v1/users/{id}). A terminal REJECTED means the provider evaluated the data and declined it (note: verification_status may still read unverified — gate on status); the rejection reason is delivered ONLY in the user.verification.failed webhook (data.reasons[]) — GET /v1/users/{id} never exposes it. If REJECTED, fix the data with PUT /v1/users/{id} and ask your Kira contact to re-trigger verification.
status and verification_status can contradict each other. Gate your state machine on status only; treat verification_status as advisory. status lifecycle: CREATED → VERIFYING → REVIEW → VERIFIED (terminal REJECTED). You may also see ACTIVE in some tenants — treat unknown values as non-terminal.

Virtual Account

The API collapses activating/active to approved; the older pending/activating/active ladder is wrong for this pin. approved ≠ funds-ready. Detect a truly-active VA via account_number being a real account number — non-null AND != "PENDING-ACT-ACCOUNT" (the sentinel is ACT-only; international / crypto VAs show account_number: null until provisioned). On an active ACT VA, GET /v1/virtual-accounts/{id}/balance returns 200 with available_balance (while activating it returns 400); confirm funding via account_number + virtual_account.deposit_* webhooks. On an active ACT VA, GET returns bank: null with payment_methods: WIRE+ACH.
Constraint: only one ACT VA per user. Re-creating returns 409 Conflict. Read the existing VA via List/Get.

Payout

Casing and field names differ between create and read. The payout 201 create response returns status: "created" (lowercase) and identifies the payout with an id field. But GET /v1/payouts/{id} and GET /v1/payouts return the status UPPERCASE (e.g. CREATED, KYT_PENDING) and name that same field payout_id, not id. Always compare statuses case-insensitively, and read the identifier from id on the create response but payout_id on reads.
A freshly created payout commonly transitions created → KYT_PENDING (a Know-Your-Transaction compliance screen) before processing. Treat KYT_PENDING as a non-terminal, in-flight state — keep polling GET /v1/payouts/{id} or wait for webhooks; do not treat it as failure. Preview requires a funded VA. With a $0 balance you get 400 "Insufficient balance" — fund the VA first with a simulate-deposit, which on the fiat wire rail credits its available balance (a payout then debits it), so deposit → balance → payout is a real cycle and GET /…/balance reflects the credit (possibly after a few seconds). The balance also includes any opening sandbox float, so a payout can also succeed without a prior deposit. Balance behavior is not uniform across every rail, so treat the virtual_account.deposit_funds_received webhook as the rail-independent signal — and note the simulated deposit also appears in GET /…/{id}/deposits.

Request body shapes (the things AI agents commonly hallucinate)

Create individual user — international (ACT route, no documents)

Gotchas:
  • verification_mode must be "automatic"
  • immigration_status (non-US ACT) is exactly one of: "U.S. Citizen", "Permanent U.S. Resident", "Lawful Permanent Resident of U.S.", "Non-Permanent U.S. Resident", "Non-Resident of U.S."
  • Documents are file uploads: government IDs / business docs need identifying_information[].documents: [{type:"front"|"back", file:"data:image/jpeg;base64,…"}] (HTTPS URL ok on 2026-04-14+). Tax IDs (ssn/itin/ein/curp/rfc/...) are number-only. back waived for passports. Do NOT send ssn for non-US individuals, or ein for non-US businesses (use international_entity_type)
  • additional_info boolean fields are strings ("Yes" / "No", case-sensitive), not booleans. For non-US-citizen users (like this example) they are required: omitting them surfaces in missing_fields and leaves the ACT product ineligible. US-citizen applications don’t include this block at all
  • nationality and address_country use alpha-3 ISO codes (MEX, USA)
  • account_purpose accepted enum (sending any other value → 400 invalid_enum_value listing these):
receive_payments · manage_professional_income · make_payments · manage_personal_funds · investment_trading · charitable_donations · investment_purposes · operating_a_company · payments_to_friends_or_family_abroad · personal_or_living_expenses · purchase_goods_and_services · protect_wealth · receive_salary · receive_payment_for_freelancing

Create business user — USA (with EIN)

Gotchas:
  • business_industry is an array of NAICS-style enums, NOT a string. Valid example: "merchant_wholesalers_nondurable_goods". Invalid: "wholesale_trade", "trading".
  • business_type enum: llc / corporation / partnership / sole_prop / trust
  • A beneficial owner is recognized by has_ownership: true + ownership_percentage (5 or more) — NOT by role. At least one associated_persons[] entry must qualify, or associated_persons:beneficial_owner surfaces in missing_fields and verification does not trigger (blocking for a new business). USA businesses need an SSN on each associated person.
  • account_purpose uses the business enum here — receive_payments_for_goods_and_services, not the individual receive_payments (that value 400s on a business).
  • All four company-document groups must be covered — file_business_formation, file_certificate_of_good_standing, file_portfolio_statement, file_board_minutes (or equivalents; articles/bylaws cover three groups at once). Only the formation doc → the other three surface in missing_fields.
  • The Yes/No attestations high_risk_industries, is_nbfi_vasp, business_legal_history are always required for KYB. Yes on either of the first two triggers industry EDD (extra documents).

Create virtual account

Two routes: 1. USD fiat via ACT (Austin Capital Trust — US bank account):
2. Crypto stablecoin via the international-wire rail (on-chain sweep address) — bank: "slovak_savings_bank" in sandbox, bank: "portage" in prod:
Supported (destination.currency, destination.network) pairs today: (USDC, solana), (USDC, polygon), (USDT, tron), (USDT, solana), (USDT, polygon). Gotchas:
  • type must be "US_BANK" — it is the only supported type today (even crypto VAs are "US_BANK"). Any other value is not wired up — do not use.
  • Only three currencies are supported on US_BANK today: USD (fiat) and USDT / USDC (crypto). No other fiat currencies (EUR, GBP, MXN, etc.) and no other crypto assets (BTC, ETH, etc.) are accepted.
  • bank is REQUIRED for US_BANK — including crypto. Omitting it returns 400 "bank is required for US_BANK virtual accounts". The value is environment-scoped: sandbox uses slovak_savings_bank (international/crypto) or austin_capital_trust (ACT); production uses portage (international/crypto) or austin_capital_trust (ACT — domestic). Sending portage in sandbox returns 400 "Invalid bank" (authorization gate, not geography).
  • The recommended default rail is the international/crypto rail, selected via bank (slovak_savings_bank in sandbox / portage in production) — not ACT. The provider enum is act ONLY and exists as an alias for that rail (provider: "act" aliases bank: "austin_capital_trust"). There is no provider: "portage" / provider: "slovak_savings_bank" — sending either returns 400 "Expected 'act'". The service maps bank → provider (austin_capital_trust → act, portage / slovak_savings_bank → international/crypto rail).
  • mode is lowercase "fiat" or "crypto". Uppercase fails.
  • For crypto VAs, destination.address is required at create time — it’s the on-chain wallet you own where deposits will sweep. Kira does not custody this wallet.
  • One ACT VA per user. Re-creating an ACT VA returns 409 Conflict — read the existing one via List/Get.

Create recipient — Wire

Gotchas (different from User-create!):
  • Country codes are alpha-2 here (US) — different from User which uses alpha-3 (USA)
  • Field is street_name, not street
  • The bank’s address goes in account.bank_address, NOT account.address
  • Required at top level: first_name, last_name (or company_name for type: business), and email (recommended, not enforced — the API accepts an email-less create)
  • account.bank_address is REQUIRED for WIRE as a STRUCTURED OBJECT ({street_name, city, state, postal_code, country}, country 2-letter) — omitting it → 400 "account.bank_address: Required"
  • account.account_type is a discriminator: ACH / WIRE / WALLET

Preview payout

Deprecated as of July 16, 2026. New integrations should price payouts with POST /v1/quotations instead — see Itemized quotations. This section still applies to existing integrations on payout/preview.
Gotchas:
  • recipient_id at top level, NOT nested under destination
  • Crypto VAs use payment_instructions instead of recipient_id
  • There is a minimum payout amount equal to the total fees (no fee schedule endpoint exists) — Preview returns 400 "Total fees exceed or equal the payout amount" below it, so iterate up to find it

Receive webhooks

Webhooks are configured by the Kira team — there is no self-serve registration endpoint. When you need event delivery, send Kira the HTTPS URL to deliver to and they’ll register it for your account. Gotchas:
  • No retry on failed deliveries today. Your endpoint must be highly available.
  • De-duplicate by data.event_id, and verify the x-signature-sha256 HMAC over the raw body before processing.
See Webhooks for the full webhook integration guide.

Anti-patterns to avoid (do NOT do these)

  1. Do not mistake “stuck in CREATED” for a queue, and do not assume verification lands in seconds. Verification triggers on its own for a COMPLETE user, but approval is a manual step — per Kira it can take up to 24 hours (manual review — status: REVIEW — is a real outcome and resolves on Kira’s side). If your user sits in CREATED with verification_triggered: false, it is incomplete: read the missing_fields map and fill the gaps with PUT /v1/users/{id} — verification then triggers on its own. If it sits in CREATED with verification_triggered: true and no missing_fields, the usual culprit is the documents: placeholder or unreachable file values cannot be uploaded to the provider, so the submission never completes — re-send real files. A terminal REJECTED means the provider evaluated the data and declined it (while verification_status may still read unverified — gate on status); the reasons are delivered ONLY in the user.verification.failed webhook (data.reasons[]) — GET /v1/users/{id} never exposes them. After a REJECTED, fix the data and ask your Kira contact to re-trigger verification.
  2. Do not assume status: VERIFIED means the user can actually transact — also check that the VA’s GET /balance returns 200, not 400 “activating”.
  3. Do not reuse the same Idempotency-Key across logically-different requests. Generate a fresh UUID v4 per intent. Only reuse on retries of the same intent.
  4. Do not send additional_info: { "has_us_bank_account": false } — booleans get rejected. Send the capitalised string "No" (or "Yes") instead; these values are case-sensitive.
  5. Do not send provider: "portage" or provider: "slovak_savings_bank" in Create VA — the provider enum is only act. Returns 400 "Expected 'act'". Select the recommended international/crypto rail via the bank field instead (slovak_savings_bank sandbox / portage production).
  6. Do not send mode: "FIAT" — lowercase only.
  7. Do not nest recipient_id under destination in payout requests — top-level.
  8. Do not use account.address for the bank’s address on a recipient — it’s account.bank_address. (account.address would be the recipient’s address — that’s a top-level address field, not nested in account.)
  9. Do not look for provider or currency fields on a virtual account — there are none. The rail is exposed as bank, and a crypto VA’s currency is under destination.currency — identical in list and get-by-id.
  10. Do not write a single error parser that assumes one error shape — Kira’s API has multiple error shapes today ({error, details} vs {message} vs {code, error, message} vs {statusCode, error, message}). Code defensively.

Webhook signature verification

Every webhook delivery includes an HMAC-SHA256 signature in the headers. Verify it before processing:
The signature arrives in the x-signature-sha256 header (hex HMAC-SHA256 over the raw body). Verify against the raw bytes with a constant-time compare. Single delivery, NO retry on this pin. De-duplicate by data.event_idevent_id sits at data.event_id in BOTH envelope shapes (there is NO root-level event_id); payout.status_changed additionally double-nests its payload at data.data (status UPPERCASE + previous_status). Make processing idempotent.

Known event types (subscribe in your handler)

  • user.created
  • user.updated / user.status_changed / user.verification.failed (automatic verification failed → user flips to terminal REJECTED; reason in data.reasons[]) / user.verification.accepted (verification approved) / user.document.download.failed
  • virtual_account.created
  • virtual_account.activated — the only funds-ready VA webhook; you must handle it
  • virtual_account.deposit_funds_received / virtual_account.deposit_funds_in_destination (not virtual_account.deposit.completed)
  • payout.created / payout.pending / payout.processing / payout.completed / payout.failed / payout.returned
  • payin.*, card_payment.*
See Webhooks for the full catalog, the 2 envelope shapes (standard flat, and payout.status_changed V2 with the nested data.data payload), and signing details.
Other event types may be delivered. Default to 2xx + log for unknown events; do not return 4xx (sender will not retry on 4xx).

Common errors and what they mean

See Known limitations & quirks for the full set of edge cases.

Production readiness checklist (15 items)

Before requesting production credentials, your integration must demonstrably pass the following in sandbox. Full details and an evidence template are in the production-certification matrix your Kira contact provides.
1

Acquire token

Call POST /auth and cache the token.
2

Pin version

Send X-Api-Version: 2026-04-14 on every request (optionally pin the account once if its default allows).
3

Idempotency-Key correctly applied

Fresh UUID v4 per logical request.
4

Create individual user

POST /v1/users with type: individual.
5

Create business user

POST /v1/users with type: business.
6

Read a VERIFIED user

Create a complete user; approval is manual and per Kira can take up to 24 hours (it commonly lands in REVIEW for a Kira operator first) — poll or wait for the webhook, then read it back once status is VERIFIED.
7

Create a VA

POST /v1/virtual-accounts.
8

Simulate inbound deposit

POST /v1/virtual-accounts/{id}/simulate-deposit to fund the VA — confirm via the 201, the balance increase on GET /…/balance, and the virtual_account.deposit_funds_received webhook.
9

Create a recipient

POST /v1/recipients.
10

Preview a payout

POST /v1/virtual-accounts/{id}/payout/preview.
11

Execute a payout

POST /v1/virtual-accounts/{id}/payout.
12

Receive a webhook delivery

Ask Kira to register your HTTPS URL, then confirm a signed event arrives.
13

Verify HMAC signature on incoming events

Constant-time compare on the raw body.
14

De-dup retried events

Key on data.event_id.
15

Handle one error response cleanly

Defensive parsing across error shapes.
  • Interactive API reference: API reference
  • Production-readiness checklist: the production-certification matrix provided by your Kira contact
  • Human-facing integration guide: see the Quickstart and the guides

What’s coming end of June (v2026-XX-XX)

Several gotchas in this document resolve in the next API version. Behavior changes opt-in via X-Api-Version: 2026-XX-XX:
  • GET /v1/pricing endpoint with your contracted rates
  • Unified error shape: {type, code, message, param, agent_hint}
“magic-trigger” verification emails, magic SSN/EIN tables, an 8-attempt webhook retry policy, and a separate POST /v1/documents upload endpoint have been floated in BDD specs but are NOT shipping — do not design against them. Today: verify via your Kira contact, documents go inline as base64/HTTPS in identifying_information[].documents[], and webhooks are single-delivery with no retry.
When v2026-XX-XX ships, this brief will be updated. Pin to 2026-04-14 until you’ve explicitly tested the new version.
Last updated: 2026-07-02 · API version covered: 2026-04-14 · For questions: contact your Kira integration partner via the channel they shared.
Kira routes identity verification to a specialist KYC provider whose automated flow includes a compliance questionnaire filled from the fields you send on POST /v1/users (source_of_funds, account_purpose, employment_status, occupation, expected_monthly_volume, …). Missing or unmappable questionnaire answers are now caught before submission — they appear in missing_fields and hold the user in CREATED rather than causing a rejection. A user.verification.failed therefore means the provider evaluated the submitted data and declined it; the provider’s exact reasons are forwarded verbatim in data.reasons[]. It is recoverable: fix the data with PUT /v1/users/{id}, then ask your Kira contact to re-run verification — no need to create a new user.A verification-provider call failure is different: if Kira cannot reach the provider (network / 5xx / timeout), that is a retryable system error, not a rejection — the user stays non-terminal (VERIFYING), no user.verification.failed fires, and Kira retries automatically (persistent failures are escalated to Kira’s team, who re-run the verification). Watch user.status_changed for the eventual outcome rather than treating the wait as a failure.