# Kaito Core API

Rail M2M API for partner integrations. The Rail API exposes four resource families:

- **Rails** — list enabled corridors for the authenticated client
- **Accounts** — manage destination bank accounts
- **Quotes** — create a quote for a destination account
- **Orders** — convert a quote into an order, track lifecycle, and report deposits

The Rail API is M2M-only. There is no user-interactive auth and no Bearer/JWT. Every request is authorized by a custom Lambda authorizer that:

1. Looks up the caller by `x-kaito-api-key` in DynamoDB
2. Decrypts the per-key HMAC secret via KMS (with `apiKeyId/clientId/integrationId/purpose` as encryption context)
3. Recomputes the HMAC over the canonical request and compares (`x-kaito-signature`)
4. Enforces a 300-second window against `x-kaito-timestamp`
5. On success, exposes `clientId`, `integrationId`, and `scopes` to downstream Lambdas

`clientId` is **never** taken from the request body or headers — it is derived server-side from the api-key record. Partner payloads exclude any caller-supplied client identity.


Version: 2.0.0
License: Copyright © 2026 Kaito Networks. All Rights Reserved.

## Servers

Production
```
https://api.kaito.io
```

Development
```
https://api.dev.kaito.io
```

## Security

### kaitoApiKey

Public Rail API key identifier (e.g. `rk_test_…`). Used by the authorizer
to look up the client and decrypt the per-key HMAC secret.


Type: apiKey
In: header
Name: x-kaito-api-key

### kaitoSignature

Hex-encoded HMAC-SHA256 over the canonical request string:
`<timestamp>.<METHOD>.<rawPath>.<rawQueryString>.<traceId>.<contentSha256>`
using the per-key HMAC secret. `rawPath` matches API Gateway's `rawPath`
after the `/v1` mapping key is stripped.


Type: apiKey
In: header
Name: x-kaito-signature

### kaitoTimestamp

Unix timestamp (seconds or milliseconds). Must be within ±300s of
server time. Always included in the canonical signature.


Type: apiKey
In: header
Name: x-kaito-timestamp

### kaitoTraceId

Caller-generated request id. Required on every request and included
in the canonical signature. Echoed back in the `x-kaito-trace-id`
response header and in error envelopes (`error.traceId`).


Type: apiKey
In: header
Name: x-kaito-trace-id

### kaitoContent

Hex SHA-256 of the raw request body. Required for body-carrying
methods (`POST`, `PATCH`); optional for empty-body methods
(`GET`, `HEAD`, `OPTIONS`, `DELETE`) — when omitted, the SHA-256 of
the empty string is assumed in the canonical signature.


Type: apiKey
In: header
Name: x-kaito-content

## Download OpenAPI description

[Kaito Core API](https://developer.kaito.io/_bundle/core.yaml)

## Rails

Discover enabled rail corridors for the authenticated client.

### List enabled rails for the authenticated client

 - [GET /v1/rails](https://developer.kaito.io/core/rails/listrails.md): Returns enabled rail corridors (origin/destination pairs) for the authenticated client.

Each rail describes a supported corridor (e.g. GT|PA, CRYPTO|GT) and its pricing tiers. The list is used by POST /v1/quotes to decide whether a (origin, destination) pair is allowed for this client.

## Accounts

Manage destination bank accounts for the authenticated client.

### List destination accounts for the authenticated client

 - [GET /v1/accounts](https://developer.kaito.io/core/accounts/listaccounts.md): Returns destination bank accounts for the authenticated client, optionally filtered by country. Account scope is enforced by the authorizer-resolved clientId.

### Create a destination account (validates and creates)

 - [POST /v1/accounts](https://developer.kaito.io/core/accounts/createaccount.md): Creates a destination bank account for the authenticated client.

The Rail API normalizes the partner-facing payload before invoking Portal:

- local_gt / local_pa / local_cr accounts are injected with paymentRails=[{ name: 'kaito', status: 'active' }].
- soyfri_gt accounts are pre-validated using soyFriUsername (which is mapped to username for the Portal validate Lambda) and given paymentRails=[{ name: 'soyfri', status: 'active' }] derived from soyFriUsername, soyFriValidatedName, and soyFriValidatedStatus.

If the underlying Portal flow uses Bridge and Bridge requires KYC/endorsement remediation, the account is not persisted and the API returns 409 BRIDGE_ENDORSEMENT_REQUIRED with details.endorsement and details.complianceUrl. Send the user to details.complianceUrl, complete Bridge KYC, and retry the same create request.

### Validate a destination account payload

 - [POST /v1/accounts/validate](https://developer.kaito.io/core/accounts/validateaccount.md): Validates an account payload without persisting it. Delegates to the Portal validate Lambda. For soyfri_gt, the Rail API maps soyFriUsername to username to match the Portal validator's strict schema. For other account types, the payload is forwarded as-is.

Returns data.ok plus any provider-side validation metadata.

### Get a destination account by id

 - [GET /v1/accounts/{accountId}](https://developer.kaito.io/core/accounts/getaccountbyid.md)

### Partially update a destination account

 - [PATCH /v1/accounts/{accountId}](https://developer.kaito.io/core/accounts/updateaccountbyid.md): Partial update of an account. PUT is intentionally not supported (returns 405).

### Delete a destination account

 - [DELETE /v1/accounts/{accountId}](https://developer.kaito.io/core/accounts/deleteaccountbyid.md)

## Quotes

Create quotes against destination accounts.

### Create a quote

 - [POST /v1/quotes](https://developer.kaito.io/core/quotes/createquote.md): Creates a quote for the authenticated client.

Origin is required and mutually exclusive:
- sourceCountry (ISO-3166-1 alpha-2) — fiat origin. The Rail API server-side resolves Kaito's source bank account from a per-country catalog.
- senderWalletAddress — crypto origin. The destination must be a fiat account belonging to the client.

Corridor enforcement:
- Fiat origin: a rail with originCode = sourceCountry and destinationCode =  must exist.
- Crypto origin: any rail with destinationCode =  must exist.

The target account's country is resolved from targetAccountId by listing the client's accounts. An unknown targetAccountId returns 400 INVALID_REQUEST.

## Orders

Convert a quote into an order, track lifecycle, and report client deposits.

### Convert a quote into an order

 - [POST /v1/orders](https://developer.kaito.io/core/orders/createorderfromquote.md): Creates an order from a previously created quote. The response is the minimal public order projection: orderId, quoteId, status, updatedAt, and depositInstructions when the upstream flow returns them.

### List orders for the authenticated client

 - [GET /v1/orders](https://developer.kaito.io/core/orders/listorders.md): Returns the authenticated client's orders. List items are summary-shaped (corridor and amounts); use GET /v1/orders/{orderId} for full detail (financials, deposit instructions).

GET /v1/orders?orderId=... is intentionally rejected with 400 INVALID_REQUEST — use GET /v1/orders/{orderId} for single-order lookup.

### Get order detail

 - [GET /v1/orders/{orderId}](https://developer.kaito.io/core/orders/getorderbyid.md): Returns the detail-shaped public projection of an order: orderId, status, createdAt, updatedAt, corridor, financials, onrampDeposit, offrampDeposit.

Internal workflow fields are not exposed (nextStep, operationMode, sourcePartner, destinationPartner, availableActions, history, eventStatus, downstream client blobs).

### Report that the client deposit has been sent

 - [POST /v1/orders/{orderId}/deposit-reported](https://developer.kaito.io/core/orders/reportorderdeposit.md): Body-less command. Maps internally to the Portal client action mark_deposit_sent with operatorId=rail-api. Sending a body returns 400 INVALID_REQUEST.

Transitions the order from status: initial → pending (eventStatus: order_initialized → deposit_reported_by_client).

