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.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 athttps://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.
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 sendX-Api-Version: 2026-04-14header until the account is pinned) - Reference data:
GET /v1/countriesreturns{count:250, data:[...]}. (/api/countriesand/countriesreturn403— wrong path.) - Credentials needed from Kira:
api_key,client_id(UUID),password. Provided by your Kira contact through a secured channel.
Auth flow
CallPOST /auth with only your x-api-key:
One-time setup: pin the account to v2026-04-14
target_version, not version. After this, the X-Api-Version header becomes optional. See Versioning.
Required headers — every mutating request
Endpoints requiring
Idempotency-Key:
POST /v1/usersPOST /v1/recipientsPOST /v1/virtual-accountsPOST /v1/virtual-accounts/{id}/payout
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.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.
Virtual Account
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
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)
verification_modemust 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.backwaived for passports. Do NOT sendssnfor non-US individuals, oreinfor non-US businesses (useinternational_entity_type) additional_infoboolean fields are strings ("Yes"/"No", case-sensitive), not booleans. For non-US-citizen users (like this example) they are required: omitting them surfaces inmissing_fieldsand leaves the ACT product ineligible. US-citizen applications don’t include this block at allnationalityandaddress_countryuse alpha-3 ISO codes (MEX,USA)account_purposeaccepted enum (sending any other value →400 invalid_enum_valuelisting these):
account_purpose accepted values
account_purpose accepted values
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_freelancingCreate business user — USA (with EIN)
business_industryis an array of NAICS-style enums, NOT a string. Valid example:"merchant_wholesalers_nondurable_goods". Invalid:"wholesale_trade","trading".business_typeenum:llc/corporation/partnership/sole_prop/trust- A beneficial owner is recognized by
has_ownership: true+ownership_percentage(5 or more) — NOT byrole. At least oneassociated_persons[]entry must qualify, orassociated_persons:beneficial_ownersurfaces inmissing_fieldsand verification does not trigger (blocking for a new business). USA businesses need an SSN on each associated person. account_purposeuses the business enum here —receive_payments_for_goods_and_services, not the individualreceive_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 inmissing_fields. - The
Yes/Noattestationshigh_risk_industries,is_nbfi_vasp,business_legal_historyare always required for KYB.Yeson 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):bank: "slovak_savings_bank" in sandbox, bank: "portage" in prod:
(destination.currency, destination.network) pairs today: (USDC, solana), (USDC, polygon), (USDT, tron), (USDT, solana), (USDT, polygon).
Gotchas:
typemust 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_BANKtoday: USD (fiat) and USDT / USDC (crypto). No other fiat currencies (EUR, GBP, MXN, etc.) and no other crypto assets (BTC, ETH, etc.) are accepted. bankis REQUIRED forUS_BANK— including crypto. Omitting it returns400 "bank is required for US_BANK virtual accounts". The value is environment-scoped: sandbox usesslovak_savings_bank(international/crypto) oraustin_capital_trust(ACT); production usesportage(international/crypto) oraustin_capital_trust(ACT — domestic). Sendingportagein sandbox returns400 "Invalid bank"(authorization gate, not geography).- The recommended default rail is the international/crypto rail, selected via
bank(slovak_savings_bankin sandbox /portagein production) — not ACT. Theproviderenum isactONLY and exists as an alias for that rail (provider: "act"aliasesbank: "austin_capital_trust"). There is noprovider: "portage"/provider: "slovak_savings_bank"— sending either returns400 "Expected 'act'". The service mapsbank → provider(austin_capital_trust → act,portage / slovak_savings_bank → international/crypto rail). modeis lowercase"fiat"or"crypto". Uppercase fails.- For crypto VAs,
destination.addressis 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
- Country codes are alpha-2 here (
US) — different from User which uses alpha-3 (USA) - Field is
street_name, notstreet - The bank’s address goes in
account.bank_address, NOTaccount.address - Required at top level:
first_name,last_name(orcompany_namefortype: business), andemail(recommended, not enforced — the API accepts an email-less create) account.bank_addressis REQUIRED for WIRE as a STRUCTURED OBJECT ({street_name, city, state, postal_code, country},country2-letter) — omitting it →400 "account.bank_address: Required"account.account_typeis a discriminator:ACH/WIRE/WALLET
Preview payout
recipient_idat top level, NOT nested underdestination- Crypto VAs use
payment_instructionsinstead ofrecipient_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 thex-signature-sha256HMAC over the raw body before processing.
Anti-patterns to avoid (do NOT do these)
Webhook signature verification
Every webhook delivery includes an HMAC-SHA256 signature in the headers. Verify it before processing: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_id — event_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.createduser.updated/user.status_changed/user.verification.failed(automatic verification failed → user flips to terminalREJECTED; reason indata.reasons[]) /user.verification.accepted(verification approved) /user.document.download.failedvirtual_account.createdvirtual_account.activated— the only funds-ready VA webhook; you must handle itvirtual_account.deposit_funds_received/virtual_account.deposit_funds_in_destination(notvirtual_account.deposit.completed)payout.created/payout.pending/payout.processing/payout.completed/payout.failed/payout.returnedpayin.*,card_payment.*
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.
Reference links
- 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 viaX-Api-Version: 2026-XX-XX:
GET /v1/pricingendpoint 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.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.
Why a rejection happens (transparency)
Why a rejection happens (transparency)
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.