Kaito's Rail M2M API is the partner-facing surface for moving value across corridors. A single API key + HMAC scheme lets partners discover supported rails, manage destination accounts, lock prices, create orders, and report deposits.
- Unified partner-facing endpoints for rails, beneficiary accounts, quotes, and orders.
- Standardized status model and progression for portal-parity M2M flows.
- Corridor routing across
kaito ↔ kaito(fiat-fiat) andcrypto ↔ kaito/crypto ↔ soyfri(crypto-fiat) flows. - Separation between:
- catalog (
/rails,/accounts) — setup and beneficiary management - pricing (
/quotes) — create a quote against a destination account - execution (
/orders) — convert a quote into an order - asynchronous state (
/orders/{orderId},/orders/{orderId}/deposit-reported) — tracking and client-reported deposit
- catalog (
- Create a destination account (one-time per beneficiary)
POST /v1/accounts
- Create a quote
POST /v1/quotes
- Convert the quote into an order
POST /v1/orderswith{ "quoteId": "<id>" }
- Report client deposit (body-less)
POST /v1/orders/{orderId}/deposit-reported- Maps internally to the Portal client action
mark_deposit_sentwithoperatorId=rail-api
- Track order until terminal state
GET /v1/orders/{orderId}(detail) orGET /v1/orders(summary list)
Operator actions (confirm deposit, mark completed, mark failed) live in backoffice and are intentionally not exposed to partners.
The same POST /v1/quotes endpoint covers all four flow types — the shape is determined by the request body:
| Flow | Origin | Destination | Origin signal | Destination signal |
|---|---|---|---|---|
kaito → kaito | Fiat country A | Fiat country B (kaito-owned rail) | sourceCountry: "<A>" | targetAccountId of a local_* account |
kaito → soyfri | Fiat country | SoyFri GT | sourceCountry: "<country>" | targetAccountId of a soyfri_gt account |
crypto → kaito | Wallet | Fiat country (kaito-owned rail) | senderWalletAddress | targetAccountId of a local_* account |
crypto → soyfri | Wallet | SoyFri GT | senderWalletAddress | targetAccountId of a soyfri_gt account |
sourceCountry and senderWalletAddress are mutually exclusive — sending both, or neither, returns 400 INVALID_REQUEST with details.field = "origin".
When sourceCountry is provided, the Rail API resolves Kaito's source bank account server-side from a per-country catalog. Partners never specify the source account directly.
GET /v1/rails
GET /v1/accounts(optional?country=<ISO-2>)GET /v1/accounts/{accountId}POST /v1/accountsPOST /v1/accounts/validatePATCH /v1/accounts/{accountId}DELETE /v1/accounts/{accountId}
PUT /v1/accounts/{accountId} is intentionally rejected with 405.
POST /v1/accounts follows Portal behavior for Bridge-backed countries/account types. If Bridge requires KYC/endorsement remediation, the account is not created and the API returns 409 BRIDGE_ENDORSEMENT_REQUIRED with error.details.endorsement and (when available) error.details.complianceUrl. Send the user to complianceUrl, complete Bridge KYC, and retry the same create request.
POST /v1/quotes
Quote list and quote-by-id endpoints are intentionally not exposed.
POST /v1/orders(with{ "quoteId": "<id>" })GET /v1/ordersGET /v1/orders/{orderId}POST /v1/orders/{orderId}/deposit-reported(body-less)
GET /v1/orders?orderId=... is intentionally rejected with 400 INVALID_REQUEST — use the path variant for single-order lookup.
The Rail API is M2M-only. There is no Bearer/JWT. Every request is authorized by a custom Lambda authorizer that uses an API-key identifier plus a per-key HMAC signature.
Every Rail API request must carry all of the following headers:
| Header | Purpose |
|---|---|
x-kaito-api-key | Public api-key identifier (rk_test_… / rk_live_…). Identifies the calling client. |
x-kaito-timestamp | Unix timestamp (seconds or milliseconds). Must be within ±300s of server time. |
x-kaito-trace-id | Caller-generated correlation id. Echoed in the response header and in error envelopes. |
x-kaito-signature | Hex HMAC-SHA256 of the canonical request, signed with the per-key secret. |
x-kaito-content | Hex SHA-256 of the raw request body. Required for POST/PATCH; optional for GET/HEAD/OPTIONS/DELETE (the SHA-256 of the empty string is assumed when omitted). |
A plain bearer token would conflate identity (who you are) and proof (that this request really came from you) into one credential that travels on every request — every TLS-terminating proxy log, every dev-tools tab, every observability tool sees it.
The Rail API splits the two:
x-kaito-api-keyis the public identifier. It is sent on every request. It is used by the authorizer to look up the per-key HMAC secret in DynamoDB and decrypt it via KMS (withapiKeyId/clientId/integrationId/purposeas encryption context).- The secret never leaves the partner's side. It is used only to compute the HMAC signature. The signature additionally covers the timestamp, method, path, query, body hash, and trace id — so a captured request can't be replayed past the 300-second window and a proxy can't tamper with the payload without invalidating the signature.
This is the same model as AWS SigV4, Stripe webhooks, and GitHub webhooks.
<x-kaito-timestamp>.<HTTP_METHOD>.<rawPath>.<rawQueryString>.<x-kaito-trace-id>.<contentSha256>HTTP_METHODis uppercase (e.g.POST).rawPathis the API Gateway raw path after the/v1mapping key is stripped. For example, callingPOST https://api.kaito.io/v1/quotessignsrawPath = "/quotes".rawQueryStringis the raw query string (no canonicalization). If empty, sign an empty string.contentSha256is the hex SHA-256 of the raw request body. For empty-body requests this is the SHA-256 of the empty string:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.
The signature itself is hex-encoded HMAC-SHA256 of the canonical string, computed with the per-key HMAC secret.
The authorizer returns 403 Forbidden with error.details.reason set to one of:
missing_api_key, missing_hmac_headers, missing_hmac_content, api_key_not_found, api_key_not_active, api_key_expired, missing_secret, invalid_timestamp, timestamp_out_of_tolerance, invalid_signature.
401 Unauthorized is returned by API Gateway itself when the required identity-source headers are absent before the authorizer is even invoked.
A ready-to-use Postman collection is shipped with this portal: postman/kaito-rail-api.postman_collection.json. Import it, fill in apiKeyId / hmacSecret / targetAccountId under the Variables tab, and every request is signed automatically by the collection-level pre-request script. It covers all five flow examples (kaito↔kaito both directions, kaito→soyfri, crypto→kaito, crypto→soyfri), accounts CRUD, and the deposit-reported action.
import crypto from 'node:crypto';
const apiKey = 'rk_test_...';
const secret = 'sk_...';
const method = 'POST';
const baseUrl = 'https://api.kaito.io/v1';
const path = '/quotes';
const body = JSON.stringify({
sourceCountry: 'GT',
targetAccountId: '019e4c4b-...-ba-...',
amount: 20,
});
const timestamp = Math.floor(Date.now() / 1000).toString();
const traceId = `trc_${Date.now().toString(36)}_${crypto.randomBytes(4).toString('hex')}`;
const contentSha256 = crypto.createHash('sha256').update(body).digest('hex');
const canonical = [timestamp, method, path, '', traceId, contentSha256].join('.');
const signature = crypto.createHmac('sha256', secret).update(canonical).digest('hex');
await fetch(`${baseUrl}${path}`, {
method,
headers: {
'content-type': 'application/json',
'x-kaito-api-key': apiKey,
'x-kaito-timestamp': timestamp,
'x-kaito-trace-id': traceId,
'x-kaito-content': contentSha256,
'x-kaito-signature': signature,
},
body,
});All Rail M2M errors share the same envelope:
{
"traceId": "trc_01JH8K5P3V9M2K5Q7W6J3Z1A9B",
"error": {
"code": "INVALID_REQUEST",
"message": "Missing required field",
"details": {}
}
}traceId in the body matches the response header x-kaito-trace-id and the request's x-kaito-trace-id header.
Partner-facing responses are intentionally minimal. The following internal workflow fields are never present in any Rail M2M response:
nextStep, operationMode, sourcePartner, destinationPartner, availableActions, history, eventStatus, downstream client blobs, filter echoes, action-result item wrappers.
- Partners drive the client-facing steps: create accounts, create quotes, create orders, report deposits, and observe order state.
- Operator-only steps — confirming deposits, marking orders completed or failed — remain in backoffice and are not exposed in this API.