Every connection — dashboard and API alike — goes through a dedicated OpenVPN tunnel. Nothing here is reachable from the open internet. See Network access.

Documentation

Everything you need to integrate Krypa

The full guide and API reference for the Krypa payouts (OFF_RAMP) and on-ramp (ON_RAMP) API — the pipeline, authentication, every endpoint, webhooks, client libraries, and the error code catalog. One long page, deep-linkable by section.

Using the API itself requires a partner account. This page is reference material — the live dashboard, sandbox keys, and the API endpoint are reachable only through a dedicated VPN tunnel issued during partner onboarding. Reach out via the homepage to get started.

Introduction

Programmatic crypto-to-fiat payouts, worldwide.

Krypa lets you send a single payout request and have it delivered through the best available local payment method in the destination country — SBP in Russia, SEPA in the Eurozone, UPI in India, PIX in Brazil, and more. You don't choose a route; Krypa's routing engine does, and fails over automatically if the first attempt doesn't clear.

New here? Start with Onboarding

If you just received a VPN client and an invite link, read Onboarding first — it covers connecting the VPN, accepting your invite, and a tour of the dashboard before any of the API material below is relevant.

Looking for the full commercial and onboarding overview to share internally? Download the Partnership Guide (PDF).

Environments

Every request runs against either the sandbox or live environment, selected by which API key you authenticate with — the URL is the same for both. Sandbox requests never move real money and settle instantly against simulated corridors.

Idempotency

Every POST accepts an Idempotency-Key header. Replaying the same request with the same key returns the original result instead of creating a duplicate payout — safe to retry on any network error or timeout.

Network access (VPN)

Both partners.krypa.io and api.partners.krypa.io are only reachable through your dedicated VPN tunnel.

Read this before anything else in these docs

Two hosts, same rule — neither is reachable from the open internet at all:

  • partners.krypa.io — the dashboard you're reading this in.
  • api.partners.krypa.io — the API these docs describe (/payouts, /onramp-orders, and everything else under API reference).

Every request to either one, including every POST /payouts or POST /onramp-orders call your backend makes, has to travel through your dedicated OpenVPN tunnel to Krypa. There is no public, VPN-less endpoint for either host.

Why

Krypa's partner infrastructure is deliberately never exposed on the public internet. Instead, every partner gets an individually-issued OpenVPN client during onboarding, alongside their dashboard login. Connecting that client is what makes partners.krypa.io and api.partners.krypa.io resolve and respond at all — without it, requests to either simply time out, the same way they would for anyone scanning the internet for them.

This is a deliberate security boundary, not an incidental one: it applies equally to a developer clicking around the dashboard in a browser and to your production backend calling the API server-to-server. Neither works without the tunnel up.

What this means for your integration

Your backend needs the tunnel, not just your laptop

It's easy to connect the VPN on your own machine, test the API from Postman or curl, and assume the integration is done. It isn't — whatever server or container in your infrastructure actually calls POST /payouts / POST /onramp-orders in production needs the OpenVPN client running there too, connected and healthy, or every request from it will simply fail to connect.

Practically, that means:

  • Install and keep the OpenVPN client (.ovpn config you received during onboarding) running on whichever host makes outbound calls to Krypa — typically the same backend service that owns your order/payout logic, not a developer workstation.
  • Monitor the tunnel like any other dependency your integration relies on — if it drops, calls to Krypa will start timing out, not returning an error response you can branch on.
  • Each partner's VPN client is unique to that partner and scoped so partners can't see each other's traffic. If you need a second client (e.g. a separate staging environment), ask your account manager — don't share one .ovpn file across unrelated environments.

Getting your VPN client

VPN access is issued individually during onboarding, together with your dashboard invite — see your account manager or Onboarding. API keys are created separately, by you, once you're in the dashboard — see Settings → API keys. VPN access isn't self-service from this site, precisely because it's the thing that gates access to the site.

Getting started

Create your first sandbox payout in a few minutes.

Connect your VPN first

Nothing below works without it — both partners.krypa.io (the dashboard) and api.partners.krypa.io (the API calls below) are only reachable through your dedicated VPN tunnel. See Network access if you haven't connected yet.

1. Get a sandbox key

Connect your VPN, then sign in to the dashboard

Every partner gets a dashboard at /dashboard, reachable only once your OpenVPN client is connected. If you don't have a VPN client or account yet, ask your Krypa account manager for an invite link.

Create a sandbox API key

Go to Settings → API keys, switch the environment toggle to Sandbox, and click Create key. Copy the secret — it's shown once.

Check which corridors are live

Not every country/currency/method combination is available yet. Call GET /corridors?status=live or check Dashboard → Corridors before you build against a specific route.

2. Create a payout

curl -X POST https://api.partners.krypa.io/v1/payouts \
  -H "Authorization: Bearer $KRYPA_SANDBOX_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "order-4471",
    "from_currency": "USDT",
    "from_amount": "500.00",
    "to_currency": "RUB",
    "payment_method": "sbp",
    "recipient": {
      "type": "phone_bank",
      "phone": "+79991234567",
      "bank_bik": "044525225"
    }
  }'

The response comes back with status: "pending" immediately. Krypa moves it through processing to a terminal state asynchronously — see How it works for the full state machine, and Webhooks for how to get notified without polling.

3. Watch it move

Open Dashboard → Transactions (in Sandbox mode) and find order-4471 — sandbox payouts settle in seconds so you can see the full lifecycle without waiting.

How it works

The pipeline, the state machines, and webhook delivery — both directions.

The pipeline (OFF_RAMP)

You send one payout request. Krypa detects the country, currency, and payment method, then routes it through the best available path in that region — with automatic failover if the first attempt doesn't clear.

MerchantPOST /v1/payoutsKrypa APIvalidate, idempotencyRouting engineSBP, SEPA, UPI, PIX…Recipientreceives fiat

You never integrate a payment rail directly, and you never see rail-specific error codes or formats — Krypa normalizes all of that into the single Payout object described in the API reference.

On-ramp orders (below) run the mirror image of this pipeline: instead of Krypa pushing fiat out through a route, your user pushes fiat in, Krypa detects and confirms it, and releases crypto to the address you specified.

The payout state machine (OFF_RAMP)

A payout has exactly six possible statuses, and the transitions between them are fixed — a terminal state never changes.

cancelpendingprocessingcompletedfailedexpiredcancelled
  • pending — received, route not yet assigned. Cancellable via POST /payouts/{id}/cancel.
  • processing — handed to a route. No longer cancellable.
  • completed — funds delivered. to_amount is final.
  • failed — the route failed; failure_reason is populated. See Errors for the code catalog.
  • expired — no route cleared in time.
  • cancelled — cancelled by you while still pending.

Poll GET /payouts/{id} if you need to check a specific payout on demand, but for anything at scale, use webhooks instead — see below.

The on-ramp order state machine (ON_RAMP)

An on-ramp order shares the same six statuses, but pending means something different, and one transition doesn't go through processing at all:

  • pending — order created, payment_instructions returned, waiting for your user's fiat to arrive. Cancellable via POST /onramp-orders/{id}/cancel.
  • processing — fiat detected and being confirmed. No longer cancellable.
  • completed — crypto released to to_address. to_amount is final.
  • failed — confirmation failed after fiat arrived; failure_reason is populated. See Errors for the code catalog.
  • expired — the expected fiat amount never arrived in time. This is a direct pending → expired transition — unlike a payout, an on-ramp order that expires never passes through processing.
  • cancelled — cancelled by you while still pending.

Poll GET /onramp-orders/{id} if you need to check a specific order on demand, but for anything at scale, use webhooks instead — see below.

Webhook delivery, both directions

Every status transition on either resource fires a webhook to your configured endpoint: payout.updated for payouts (including the initial pending → processing jump), onramp_order.updated for on-ramp orders (including the direct pending → expired jump). Delivery is retried with exponential backoff if your endpoint doesn't return a 2xx.

Attempt 1 · POST payout.updated (immediately)no 2xx / timeout → retryAttempt 2 · POST payout.updated (+1 min)no 2xx / timeout → retryAttempt 3 · POST payout.updated (+5 min)no 2xx / timeout → retryAttempt 4 · POST payout.updated (+30 min)no 2xx / timeout → retryAttempt 5 · POST payout.updated (+2 hr)no 2xx / timeout → retryAttempt 6 · POST payout.updated (+6 hr)2xx at any attemptdelivered, stopAll 6 attempts failedmarked failed — resend manually from Webhooks

Full signature verification and payload shape for both event types are covered in Webhooks.

Settlement models

Two ways to settle OFF_RAMP volume with Krypa — on-chain or deposit-based.

Krypa offers two settlement models for OFF_RAMP (USDT → fiat). They differ in who holds the deposit and where the blockchain transaction happens — not in the payout API itself. Both can run in parallel across different corridors or volumes, and either one is arranged directly with your account manager during onboarding.

Model A — On-chain settlement

You send USDT on-chain per transaction. Krypa guarantees fiat delivery.

① You send 1,000 USDT to our wallet (TRC-20/ERC-20)
② We verify the transaction on-chain
③ We execute the fiat payout to your user (e.g. via SBP)
④ Done

Krypa places a USDT deposit on your platform (a modest starting amount, set with your account manager). That deposit is the guarantee: if Krypa fails to deliver fiat, you retain the corresponding amount from Krypa's deposit.

AspectDetail
Who depositsKrypa deposits on your side
Deposit purposeGuarantee of fiat delivery
BlockchainEvery transaction is on-chain
Your riskZero — USDT moves only when you send it
Speed3–10 min (block confirmation + payout)
Gas feesPer transaction (sender pays)

Best for partners who need full on-chain transparency or an auditable transaction trail for compliance.

Model B — Deposit-based settlement

No blockchain between us. Instant processing from your prepaid deposit — this is the model the sandbox and POST /payouts exercise today.

① You send a payout request via the API
② We deduct the amount from your deposit balance
③ We execute the fiat payout to your user
④ Done

You top up a USDT deposit on the Krypa platform (a modest starting amount, set with your account manager). Each payout request deducts from it internally — no on-chain transaction between us. Once the deposit is used up, order flow pauses automatically until you top up again; see Balance in your dashboard.

AspectDetail
Who depositsYou deposit on our side
Deposit purposePrepayment for payouts
BlockchainNone between us
Your riskLimited to your deposit amount
SpeedInstant — no block confirmations
Gas feesNone

Best for partners who prioritize speed and operational simplicity.

Choosing a model

The payout API is identical either way

POST /payouts, statuses, and webhooks work the same regardless of settlement model — the difference is purely in how the underlying deposit is funded and held. Model B is what's live in sandbox today; Model A is set up directly with your account manager.

Most partners start with Model B for speed and simplicity, then optionally add Model A for specific high-value or compliance-sensitive corridors. Talk to your account manager about which fits your onboarding — see the Partnership Guide for the full commercial overview.

Authentication

Bearer API keys, scopes, and environments.

All requests are authenticated with a bearer API key issued per environment from Dashboard → Settings → API keys.

curl https://api.partners.krypa.io/v1/balance \
  -H "Authorization: Bearer $KRYPA_API_KEY"

Sandbox vs. live

The API key determines the environment — the URL is identical for both. There's no separate sandbox hostname to swap out.

Scopes

ScopeCan readCan create payouts
read_onlyYesNo
read_writeYesYes

Use a read_only key anywhere you don't need to create payouts — for example, a reconciliation job that only calls GET /payouts.

Rotating a key

Keys can't be edited once created. To rotate: create a new key, update your integration to use it, then revoke the old one from Settings → API keys. Revocation is immediate — in-flight requests using the old key will start failing with 401 right away.

Keys are shown once

The full secret is only ever displayed at creation time. Krypa stores and can only show you the key's prefix and last 4 characters afterward.

API reference

Every endpoint, generated from the OpenAPI contract.

Every request below goes through your VPN tunnel to api.partners.krypa.io/v1 — see Network access if you haven't set that up yet, and Authentication for how bearer keys and sandbox/live selection work. All request/response bodies, parameters, and examples on the pages below are generated directly from the same openapi.yaml contract the Go backend is built against, so they can't drift from the real API.

All endpoints

MethodPathOperation
POST/payoutsCreate a payout
GET/payoutsList payouts
GET/payouts/{id}Get a payout
POST/payouts/{id}/cancelCancel a payout
POST/onramp-ordersCreate an on-ramp order
GET/onramp-ordersList on-ramp orders
GET/onramp-orders/{id}Get an on-ramp order
POST/onramp-orders/{id}/cancelCancel an on-ramp order
GET/corridorsList corridors
GET/currenciesList currencies
GET/payment-methodsList payment methods
GET/balanceGet balance

Create a payout (OFF_RAMP)

Creates a payout request and returns immediately with status `pending`. Krypa selects a route and moves the payout through `processing` to a terminal state (`completed`, `failed`, or `expired`) asynchronously — subscribe to webhooks or poll `GET /payouts/{id}` to track it.

POST
/payouts

Creates a payout request and returns immediately with status pending. Krypa selects a route and moves the payout through processing to a terminal state (completed, failed, or expired) asynchronously — subscribe to webhooks or poll GET /payouts/{id} to track it.

Authorization

bearerAuth
AuthorizationBearer <token>

API key issued from Dashboard → Settings → API keys. Sandbox and live keys are separate; which one you send determines the environment for that request.

In: header

Header Parameters

Idempotency-Key*string

A unique key (e.g. a UUID v4) for this request, valid for 24 hours.

Length1 <= length <= 255

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

external_id*string

Your own order/reference ID. Echoed back on the payout and on every webhook — use it to reconcile against your own system.

from_currency*string

Source crypto asset — one of the codes returned by GET /currencies?type=crypto, e.g. USDT.

from_amount*Money

Decimal amount as a string (never a float), e.g. "500.00".

Match^\d+(\.\d{1,8})?$
to_currency*string

Destination fiat currency — one of the codes returned by GET /currencies?type=fiat, e.g. RUB.

payment_method*string

One of the codes returned by GET /payment-methods?direction=OFF_RAMP for this corridor, e.g. sbp, sepa, upi, pix.

recipient*||
webhook_url?string

Optional per-request override of your dashboard-configured webhook endpoint.

Formaturi

Response Body

application/json

application/json

application/json

application/json

application/json

application/json

application/json

application/json

curl -X POST "https://api.partners.krypa.io/v1/payouts" \
  -H "Idempotency-Key: string" \
  -H "Authorization: Bearer " \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "order-4471",
    "from_currency": "USDT",
    "from_amount": "500.00",
    "to_currency": "RUB",
    "payment_method": "sbp",
    "recipient": {
      "type": "phone_bank",
      "phone": "+79991234567",
      "bank_bik": "044525225"
    }
  }'
{
  "id": "string",
  "external_id": "string",
  "status": "pending",
  "from_currency": "string",
  "from_amount": "string",
  "to_currency": "string",
  "to_amount": "string",
  "payment_method": "string",
  "recipient": {
    "type": "phone_bank",
    "phone": "string",
    "bank_bik": "string"
  },
  "failure_reason": "string",
  "estimated_completion": "string",
  "created_at": "2019-08-24T14:15:22Z",
  "updated_at": "2019-08-24T14:15:22Z"
}

List payouts

Returns your payouts, newest first, optionally filtered by status, currency pair, or creation time. Cursor-paginated — pass the `next_cursor` from one page as `cursor` on the next.

GET
/payouts

Returns your payouts, newest first, optionally filtered by status, currency pair, or creation time. Cursor-paginated — pass the next_cursor from one page as cursor on the next.

Authorization

bearerAuth
AuthorizationBearer <token>

API key issued from Dashboard → Settings → API keys. Sandbox and live keys are separate; which one you send determines the environment for that request.

In: header

Query Parameters

cursor?string
limit?integer

Page size, 1–100. Out of range returns 400 validation_error.

Range1 <= value <= 100
Default20
status?string

Shared by both Payout and OnRampOrder — same set of values, but what pending means and which transitions apply differ by direction:

For a payout: pending → processing → one of completed / failed / expired. pending can also move directly to cancelled via the cancel endpoint. No other transitions are possible — a terminal state never changes. Here pending means "route not yet assigned".

For an on-ramp order: the same pending → processing → terminal path applies once fiat is detected, but pending can also move directly to expired if the expected fiat never arrives in time — it does not pass through processing first. Here pending means "waiting for your user's fiat to arrive".

See the How it works guide (/docs/how-it-works) for the full state diagram of each.

Value in

  • "pending"
  • "processing"
  • "completed"
  • "failed"
  • "expired"
  • "cancelled"
from_currency?string
to_currency?string
created_after?string
Formatdate-time
created_before?string
Formatdate-time

Response Body

application/json

application/json

application/json

application/json

curl -X GET "https://api.partners.krypa.io/v1/payouts" \
  -H "Authorization: Bearer "
{
  "data": [
    {
      "id": "string",
      "external_id": "string",
      "status": "pending",
      "from_currency": "string",
      "from_amount": "string",
      "to_currency": "string",
      "to_amount": "string",
      "payment_method": "string",
      "recipient": {
        "type": "phone_bank",
        "phone": "string",
        "bank_bik": "string"
      },
      "failure_reason": "string",
      "estimated_completion": "string",
      "created_at": "2019-08-24T14:15:22Z",
      "updated_at": "2019-08-24T14:15:22Z"
    }
  ],
  "has_more": true,
  "next_cursor": "string"
}

Retrieve a payout

Fetch a single payout by its Krypa ID. Poll this or subscribe to `payout.updated` to track a payout through to a terminal state.

GET
/payouts/{id}

Fetch a single payout by its Krypa ID. Poll this or subscribe to payout.updated to track a payout through to a terminal state.

Authorization

bearerAuth
AuthorizationBearer <token>

API key issued from Dashboard → Settings → API keys. Sandbox and live keys are separate; which one you send determines the environment for that request.

In: header

Path Parameters

id*string

Response Body

application/json

application/json

application/json

application/json

curl -X GET "https://api.partners.krypa.io/v1/payouts/string" \
  -H "Authorization: Bearer "
{
  "id": "string",
  "external_id": "string",
  "status": "pending",
  "from_currency": "string",
  "from_amount": "string",
  "to_currency": "string",
  "to_amount": "string",
  "payment_method": "string",
  "recipient": {
    "type": "phone_bank",
    "phone": "string",
    "bank_bik": "string"
  },
  "failure_reason": "string",
  "estimated_completion": "string",
  "created_at": "2019-08-24T14:15:22Z",
  "updated_at": "2019-08-24T14:15:22Z"
}

Cancel a payout

Only payouts still in `pending` can be cancelled. Once a payout moves to `processing` it has already been handed to a route and can no longer be cancelled — wait for a terminal state instead.

POST
/payouts/{id}/cancel

Only payouts still in pending can be cancelled. Once a payout moves to processing it has already been handed to a route and can no longer be cancelled — wait for a terminal state instead.

Authorization

bearerAuth
AuthorizationBearer <token>

API key issued from Dashboard → Settings → API keys. Sandbox and live keys are separate; which one you send determines the environment for that request.

In: header

Path Parameters

id*string

Response Body

application/json

application/json

application/json

application/json

application/json

curl -X POST "https://api.partners.krypa.io/v1/payouts/string/cancel" \
  -H "Authorization: Bearer "
{
  "id": "string",
  "external_id": "string",
  "status": "pending",
  "from_currency": "string",
  "from_amount": "string",
  "to_currency": "string",
  "to_amount": "string",
  "payment_method": "string",
  "recipient": {
    "type": "phone_bank",
    "phone": "string",
    "bank_bik": "string"
  },
  "failure_reason": "string",
  "estimated_completion": "string",
  "created_at": "2019-08-24T14:15:22Z",
  "updated_at": "2019-08-24T14:15:22Z"
}
API referenceOn-ramp orders

Create an on-ramp order (ON_RAMP)

Creates an order and returns immediately with status `pending`, together with `payment_instructions` — show these to your user so they know where to send fiat to (e.g. an SBP phone number, or bank account details). Once Krypa detects and confirms the incoming fiat, the order moves to `processing`, then to a terminal state (`completed` — crypto released to `to_address` — or `failed` / `expired`) asynchronously. Subscribe to webhooks or poll `GET /onramp-orders/{id}` to track it.

POST
/onramp-orders

Creates an order and returns immediately with status pending, together with payment_instructions — show these to your user so they know where to send fiat to (e.g. an SBP phone number, or bank account details). Once Krypa detects and confirms the incoming fiat, the order moves to processing, then to a terminal state (completed — crypto released to to_address — or failed / expired) asynchronously. Subscribe to webhooks or poll GET /onramp-orders/{id} to track it.

Authorization

bearerAuth
AuthorizationBearer <token>

API key issued from Dashboard → Settings → API keys. Sandbox and live keys are separate; which one you send determines the environment for that request.

In: header

Header Parameters

Idempotency-Key*string

A unique key (e.g. a UUID v4) for this request, valid for 24 hours.

Length1 <= length <= 255

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

external_id*string

Your own order/reference ID. Echoed back on the order and on every webhook.

from_currency*string

Source fiat currency your user will pay with — one of the codes returned by GET /currencies?type=fiat, e.g. RUB.

from_amount*Money

Expected fiat amount. The order expires if this exact amount (within a small tolerance) isn't received in time.

Match^\d+(\.\d{1,8})?$
to_currency*string

Destination crypto asset — one of the codes returned by GET /currencies?type=crypto, e.g. USDT.

to_network*string

Network to release to_currency on — one of the networks listed for that currency by GET /currencies, e.g. TRC20.

to_address*string

Wallet address to release crypto to once fiat is confirmed received.

payment_method*string

One of the codes returned by GET /payment-methods?direction=ON_RAMP for this corridor, e.g. sbp, bank_transfer.

Response Body

application/json

application/json

application/json

application/json

application/json

application/json

application/json

curl -X POST "https://api.partners.krypa.io/v1/onramp-orders" \
  -H "Idempotency-Key: string" \
  -H "Authorization: Bearer " \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "topup-9931",
    "from_currency": "RUB",
    "from_amount": "50000.00",
    "to_currency": "USDT",
    "to_network": "TRC20",
    "to_address": "TXYZ1234567890abcdefgh",
    "payment_method": "sbp"
  }'
{
  "id": "string",
  "external_id": "string",
  "status": "pending",
  "from_currency": "string",
  "from_amount": "string",
  "to_currency": "string",
  "to_amount": "string",
  "to_network": "string",
  "to_address": "string",
  "payment_method": "string",
  "payment_instructions": {
    "type": "sbp",
    "phone": "string",
    "bank_name": "string"
  },
  "failure_reason": "string",
  "created_at": "2019-08-24T14:15:22Z",
  "updated_at": "2019-08-24T14:15:22Z"
}
API referenceOn-ramp orders

List on-ramp orders

Returns your on-ramp orders, newest first, optionally filtered by status, currency pair, or creation time. Cursor-paginated — pass the `next_cursor` from one page as `cursor` on the next.

GET
/onramp-orders

Returns your on-ramp orders, newest first, optionally filtered by status, currency pair, or creation time. Cursor-paginated — pass the next_cursor from one page as cursor on the next.

Authorization

bearerAuth
AuthorizationBearer <token>

API key issued from Dashboard → Settings → API keys. Sandbox and live keys are separate; which one you send determines the environment for that request.

In: header

Query Parameters

cursor?string
limit?integer

Page size, 1–100. Out of range returns 400 validation_error.

Range1 <= value <= 100
Default20
status?string

Shared by both Payout and OnRampOrder — same set of values, but what pending means and which transitions apply differ by direction:

For a payout: pending → processing → one of completed / failed / expired. pending can also move directly to cancelled via the cancel endpoint. No other transitions are possible — a terminal state never changes. Here pending means "route not yet assigned".

For an on-ramp order: the same pending → processing → terminal path applies once fiat is detected, but pending can also move directly to expired if the expected fiat never arrives in time — it does not pass through processing first. Here pending means "waiting for your user's fiat to arrive".

See the How it works guide (/docs/how-it-works) for the full state diagram of each.

Value in

  • "pending"
  • "processing"
  • "completed"
  • "failed"
  • "expired"
  • "cancelled"
from_currency?string
to_currency?string
created_after?string
Formatdate-time
created_before?string
Formatdate-time

Response Body

application/json

application/json

application/json

application/json

curl -X GET "https://api.partners.krypa.io/v1/onramp-orders" \
  -H "Authorization: Bearer "
{
  "data": [
    {
      "id": "string",
      "external_id": "string",
      "status": "pending",
      "from_currency": "string",
      "from_amount": "string",
      "to_currency": "string",
      "to_amount": "string",
      "to_network": "string",
      "to_address": "string",
      "payment_method": "string",
      "payment_instructions": {
        "type": "sbp",
        "phone": "string",
        "bank_name": "string"
      },
      "failure_reason": "string",
      "created_at": "2019-08-24T14:15:22Z",
      "updated_at": "2019-08-24T14:15:22Z"
    }
  ],
  "has_more": true,
  "next_cursor": "string"
}
API referenceOn-ramp orders

Retrieve an on-ramp order

Fetch a single on-ramp order by its Krypa ID. Poll this or subscribe to `onramp_order.updated` to track an order through to a terminal state.

GET
/onramp-orders/{id}

Fetch a single on-ramp order by its Krypa ID. Poll this or subscribe to onramp_order.updated to track an order through to a terminal state.

Authorization

bearerAuth
AuthorizationBearer <token>

API key issued from Dashboard → Settings → API keys. Sandbox and live keys are separate; which one you send determines the environment for that request.

In: header

Path Parameters

id*string

Response Body

application/json

application/json

application/json

application/json

curl -X GET "https://api.partners.krypa.io/v1/onramp-orders/string" \
  -H "Authorization: Bearer "
{
  "id": "string",
  "external_id": "string",
  "status": "pending",
  "from_currency": "string",
  "from_amount": "string",
  "to_currency": "string",
  "to_amount": "string",
  "to_network": "string",
  "to_address": "string",
  "payment_method": "string",
  "payment_instructions": {
    "type": "sbp",
    "phone": "string",
    "bank_name": "string"
  },
  "failure_reason": "string",
  "created_at": "2019-08-24T14:15:22Z",
  "updated_at": "2019-08-24T14:15:22Z"
}
API referenceOn-ramp orders

Cancel an on-ramp order

Only orders still in `pending` (fiat not yet received) can be cancelled. Once fiat arrives and the order moves to `processing`, it can no longer be cancelled.

POST
/onramp-orders/{id}/cancel

Only orders still in pending (fiat not yet received) can be cancelled. Once fiat arrives and the order moves to processing, it can no longer be cancelled.

Authorization

bearerAuth
AuthorizationBearer <token>

API key issued from Dashboard → Settings → API keys. Sandbox and live keys are separate; which one you send determines the environment for that request.

In: header

Path Parameters

id*string

Response Body

application/json

application/json

application/json

application/json

application/json

curl -X POST "https://api.partners.krypa.io/v1/onramp-orders/string/cancel" \
  -H "Authorization: Bearer "
{
  "id": "string",
  "external_id": "string",
  "status": "pending",
  "from_currency": "string",
  "from_amount": "string",
  "to_currency": "string",
  "to_amount": "string",
  "to_network": "string",
  "to_address": "string",
  "payment_method": "string",
  "payment_instructions": {
    "type": "sbp",
    "phone": "string",
    "bank_name": "string"
  },
  "failure_reason": "string",
  "created_at": "2019-08-24T14:15:22Z",
  "updated_at": "2019-08-24T14:15:22Z"
}
API referenceReference data

List supported corridors

Returns every `(direction, currency pair, country, payment method)` combination Krypa can route today, plus corridors that are `expanding` or `planned`, each with its transaction limits. Filter to `status=live` to get exactly what you can route through right now. Resolve this at request time, don't hardcode a corridor list — Krypa adds coverage on an ongoing basis.

GET
/corridors

Returns every (direction, currency pair, country, payment method) combination Krypa can route today, plus corridors that are expanding or planned, each with its transaction limits. Filter to status=live to get exactly what you can route through right now. Resolve this at request time, don't hardcode a corridor list — Krypa adds coverage on an ongoing basis.

Authorization

bearerAuth
AuthorizationBearer <token>

API key issued from Dashboard → Settings → API keys. Sandbox and live keys are separate; which one you send determines the environment for that request.

In: header

Query Parameters

direction?string

Value in

  • "OFF_RAMP"
  • "ON_RAMP"
status?string

Value in

  • "live"
  • "expanding"
  • "planned"
country?string

ISO 3166-1 alpha-2

to_currency?string

Response Body

application/json

application/json

application/json

curl -X GET "https://api.partners.krypa.io/v1/corridors" \
  -H "Authorization: Bearer "
{
  "data": [
    {
      "id": "string",
      "direction": "OFF_RAMP",
      "from_currency": "string",
      "to_currency": "string",
      "country": "string",
      "payment_method": "string",
      "status": "live",
      "min_amount": "string",
      "max_amount": "string"
    }
  ]
}
API referenceReference data

List supported currencies

Every crypto and fiat currency Krypa recognizes anywhere in the system, with display precision. This is the authoritative set of valid values for `from_currency`/`to_currency` on both `/payouts` and `/onramp-orders` — validate against it instead of hardcoding a currency list, since it grows as Krypa expands.

GET
/currencies

Every crypto and fiat currency Krypa recognizes anywhere in the system, with display precision. This is the authoritative set of valid values for from_currency/to_currency on both /payouts and /onramp-orders — validate against it instead of hardcoding a currency list, since it grows as Krypa expands.

Authorization

bearerAuth
AuthorizationBearer <token>

API key issued from Dashboard → Settings → API keys. Sandbox and live keys are separate; which one you send determines the environment for that request.

In: header

Query Parameters

type?string

Value in

  • "crypto"
  • "fiat"

Response Body

application/json

application/json

application/json

curl -X GET "https://api.partners.krypa.io/v1/currencies" \
  -H "Authorization: Bearer "
{
  "data": [
    {
      "code": "string",
      "type": "crypto",
      "decimals": 0,
      "networks": [
        "string"
      ]
    }
  ]
}
API referenceReference data

List supported payment methods

Every local payment method Krypa can route through, with a human-readable label and which direction(s) it supports. This is the authoritative set of valid values for `payment_method` on both `/payouts` and `/onramp-orders` — a given method is only usable for a specific corridor if `GET /corridors` also lists it there.

GET
/payment-methods

Every local payment method Krypa can route through, with a human-readable label and which direction(s) it supports. This is the authoritative set of valid values for payment_method on both /payouts and /onramp-orders — a given method is only usable for a specific corridor if GET /corridors also lists it there.

Authorization

bearerAuth
AuthorizationBearer <token>

API key issued from Dashboard → Settings → API keys. Sandbox and live keys are separate; which one you send determines the environment for that request.

In: header

Query Parameters

direction?string

Value in

  • "OFF_RAMP"
  • "ON_RAMP"

Response Body

application/json

application/json

application/json

curl -X GET "https://api.partners.krypa.io/v1/payment-methods" \
  -H "Authorization: Bearer "
{
  "data": [
    {
      "code": "string",
      "label": "string",
      "directions": [
        "OFF_RAMP"
      ]
    }
  ]
}

Get your current balance

Your current prefunded balance and available credit limit, in the environment selected by your API key. Used to check headroom before creating a payout, or to reconcile against your own ledger.

GET
/balance

Your current prefunded balance and available credit limit, in the environment selected by your API key. Used to check headroom before creating a payout, or to reconcile against your own ledger.

Authorization

bearerAuth
AuthorizationBearer <token>

API key issued from Dashboard → Settings → API keys. Sandbox and live keys are separate; which one you send determines the environment for that request.

In: header

Response Body

application/json

application/json

application/json

curl -X GET "https://api.partners.krypa.io/v1/balance" \
  -H "Authorization: Bearer "
{
  "available": "string",
  "currency": "string",
  "credit_limit": "string"
}

A payout's status changed (OFF_RAMP)

Sent every time a payout transitions status — including the transition into `processing` right after creation. Delivered to the endpoint configured in your dashboard, signed and retried per the policy described in the Webhooks guide (/docs/webhooks).

Sent every time a payout transitions status — including the transition into processing right after creation. Delivered to the endpoint configured in your dashboard, signed and retried per the policy described in the Webhooks guide (/docs/webhooks).

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

id*string

Unique event ID — dedupe on this if you receive it more than once.

type*string

Always the literal string payout.updated — use this to distinguish from onramp_order.updated if both are delivered to the same endpoint.

created_at*string
Formatdate-time
data*

Response Body

Example Requests

POST/payout.updated

An on-ramp order's status changed (ON_RAMP)

Sent every time an on-ramp order transitions status — including the transition into `processing` once incoming fiat is confirmed, and the direct `pending` → `expired` transition if fiat never arrives. Same delivery, signing, and retry policy as `payout.updated` — see the Webhooks guide (/docs/webhooks).

Sent every time an on-ramp order transitions status — including the transition into processing once incoming fiat is confirmed, and the direct pending → expired transition if fiat never arrives. Same delivery, signing, and retry policy as payout.updated — see the Webhooks guide (/docs/webhooks).

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

id*string

Unique event ID — dedupe on this if you receive it more than once.

type*string

Always the literal string onramp_order.updated — use this to distinguish from payout.updated if both are delivered to the same endpoint.

created_at*string
Formatdate-time
data*

Response Body

Example Requests

POST/onramp_order.updated

Webhooks

Subscribe to real-time status updates instead of polling, for both directions.

Configure your endpoint at Dashboard → Settings → Webhooks. Krypa sends a POST request there every time a payout's status changes (payout.updated) or an on-ramp order's status changes (onramp_order.updated) — both event types are delivered to the same URL; use type to tell them apart.

Payload

A payout.updated event:

{
  "id": "evt_9c2f1a3b",
  "type": "payout.updated",
  "created_at": "2026-09-03T10:14:22Z",
  "data": {
    "id": "pay_8f3a2c1e",
    "external_id": "order-4471",
    "status": "completed",
    "from_currency": "USDT",
    "from_amount": "500.00",
    "to_currency": "RUB",
    "to_amount": "47500.00",
    "payment_method": "sbp",
    "created_at": "2026-09-03T10:12:01Z",
    "updated_at": "2026-09-03T10:14:22Z"
  }
}

An onramp_order.updated event — same envelope, data is an OnRampOrder instead of a Payout:

{
  "id": "evt_2b8e4f1c",
  "type": "onramp_order.updated",
  "created_at": "2026-09-03T10:20:05Z",
  "data": {
    "id": "onr_8f3a2c1e",
    "external_id": "topup-9931",
    "status": "completed",
    "from_currency": "RUB",
    "from_amount": "50000.00",
    "to_currency": "USDT",
    "to_amount": "486.20",
    "to_network": "TRC20",
    "to_address": "TXYZ1234567890abcdefgh",
    "payment_method": "sbp",
    "created_at": "2026-09-03T10:15:41Z",
    "updated_at": "2026-09-03T10:20:05Z"
  }
}

Dedupe on id — the same event may be delivered more than once (see retries below). Use data.external_id to reconcile against your own order records, and always treat data.status as the source of truth over whichever status you last observed.

Per-request override

POST /payouts accepts an optional webhook_url field that overrides your dashboard-configured endpoint for that one payout only — useful if a specific integration or environment needs its events routed somewhere different. Everything else (signing, retries, payload shape) is identical. There's no equivalent field on POST /onramp-orders yet — on-ramp events always go to your dashboard-configured endpoint.

Verifying signatures

Every delivery includes an X-Krypa-Signature header: an HMAC-SHA256 of the raw request body, signed with the signing secret shown on the Webhooks page.

import crypto from "node:crypto";

function isValidSignature(rawBody, signatureHeader, secret) {
  const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}

Verify before you parse

Compute the signature over the exact raw bytes of the request body, before any JSON parsing — re-serializing the parsed object can produce a different byte sequence and fail verification even for a genuine event.

Retries

If your endpoint doesn't respond with a 2xx status (or doesn't respond at all within a few seconds), Krypa retries with exponential backoff, up to 6 attempts in total:

Attempt 1 · POST payout.updated (immediately)no 2xx / timeout → retryAttempt 2 · POST payout.updated (+1 min)no 2xx / timeout → retryAttempt 3 · POST payout.updated (+5 min)no 2xx / timeout → retryAttempt 4 · POST payout.updated (+30 min)no 2xx / timeout → retryAttempt 5 · POST payout.updated (+2 hr)no 2xx / timeout → retryAttempt 6 · POST payout.updated (+6 hr)2xx at any attemptdelivered, stopAll 6 attempts failedmarked failed — resend manually from Webhooks

After the final attempt, the delivery is marked failed — visible, along with every attempt, in Dashboard → Settings → Webhooks. You can trigger an out-of-band redelivery from there at any time with Resend.

Acknowledging

Return any 2xx status code as soon as you've durably recorded the event — don't do slow work (calling other services, sending emails) in the request handler itself, since a slow response looks like a timeout to Krypa and triggers an unnecessary retry.

Client libraries

Reference client implementations for Go, JavaScript, Python, and Java.

Each client below is a complete, self-contained implementation covering the whole API: payouts (OFF_RAMP), on-ramp orders (ON_RAMP), the reference catalogs (corridors/currencies/payment methods), your balance, idempotent request retries, and webhook signature verification. Copy the file for your language, drop in your API key, and every method maps 1:1 to an endpoint in the API reference.

VPN required

None of these clients can reach https://api.partners.krypa.io/v1 without your OpenVPN tunnel connected on whichever machine runs this code — including in production. See Network access before wiring one of these into your backend.

Needs github.com/google/uuid for Idempotency-Key generation (go get github.com/google/uuid) — everything else is standard library.

package krypa

import (
	"bytes"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"strings"

	"github.com/google/uuid"
)

const DefaultBaseURL = "https://api.partners.krypa.io/v1"

type Client struct {
	APIKey     string
	BaseURL    string
	HTTPClient *http.Client
}

func NewClient(apiKey string) *Client {
	return &Client{APIKey: apiKey, BaseURL: DefaultBaseURL, HTTPClient: http.DefaultClient}
}

type APIError struct {
	Code    string `json:"code"`
	Message string `json:"message"`
	Details any    `json:"details,omitempty"`
}

func (e *APIError) Error() string {
	return fmt.Sprintf("krypa: %s: %s", e.Code, e.Message)
}

type errorEnvelope struct {
	Error APIError `json:"error"`
}

type Money = string

type Recipient struct {
	Type              string `json:"type"`
	Phone             string `json:"phone,omitempty"`
	BankBIK           string `json:"bank_bik,omitempty"`
	AccountNumber     string `json:"account_number,omitempty"`
	BankCode          string `json:"bank_code,omitempty"`
	AccountHolderName string `json:"account_holder_name,omitempty"`
	CardNumber        string `json:"card_number,omitempty"`
}

type CreatePayoutRequest struct {
	ExternalID    string    `json:"external_id"`
	FromCurrency  string    `json:"from_currency"`
	FromAmount    Money     `json:"from_amount"`
	ToCurrency    string    `json:"to_currency"`
	PaymentMethod string    `json:"payment_method"`
	Recipient     Recipient `json:"recipient"`
	WebhookURL    string    `json:"webhook_url,omitempty"`
}

type Payout struct {
	ID                  string    `json:"id"`
	ExternalID          string    `json:"external_id"`
	Status              string    `json:"status"`
	FromCurrency        string    `json:"from_currency"`
	FromAmount          Money     `json:"from_amount"`
	ToCurrency          string    `json:"to_currency"`
	ToAmount            Money     `json:"to_amount"`
	PaymentMethod       string    `json:"payment_method"`
	Recipient           Recipient `json:"recipient"`
	FailureReason       *string   `json:"failure_reason,omitempty"`
	EstimatedCompletion *string   `json:"estimated_completion,omitempty"`
	CreatedAt           string    `json:"created_at"`
	UpdatedAt           string    `json:"updated_at"`
}

type PayoutList struct {
	Data       []Payout `json:"data"`
	HasMore    bool     `json:"has_more"`
	NextCursor *string  `json:"next_cursor,omitempty"`
}

type PayoutListFilters struct {
	Cursor        string
	Limit         int
	Status        string
	FromCurrency  string
	ToCurrency    string
	CreatedAfter  string
	CreatedBefore string
}

type CreateOnRampOrderRequest struct {
	ExternalID    string `json:"external_id"`
	FromCurrency  string `json:"from_currency"`
	FromAmount    Money  `json:"from_amount"`
	ToCurrency    string `json:"to_currency"`
	ToNetwork     string `json:"to_network"`
	ToAddress     string `json:"to_address"`
	PaymentMethod string `json:"payment_method"`
}

type PaymentInstructions struct {
	Type          string `json:"type"`
	Phone         string `json:"phone,omitempty"`
	BankName      string `json:"bank_name,omitempty"`
	AccountNumber string `json:"account_number,omitempty"`
	Reference     string `json:"reference,omitempty"`
}

type OnRampOrder struct {
	ID                  string               `json:"id"`
	ExternalID          string               `json:"external_id"`
	Status              string               `json:"status"`
	FromCurrency        string               `json:"from_currency"`
	FromAmount          Money                `json:"from_amount"`
	ToCurrency          string               `json:"to_currency"`
	ToAmount            Money                `json:"to_amount"`
	ToNetwork           string               `json:"to_network"`
	ToAddress           string               `json:"to_address"`
	PaymentMethod       string               `json:"payment_method"`
	PaymentInstructions PaymentInstructions  `json:"payment_instructions"`
	FailureReason       *string              `json:"failure_reason,omitempty"`
	CreatedAt           string               `json:"created_at"`
	UpdatedAt           string               `json:"updated_at"`
}

type OnRampOrderList struct {
	Data       []OnRampOrder `json:"data"`
	HasMore    bool          `json:"has_more"`
	NextCursor *string       `json:"next_cursor,omitempty"`
}

type OnRampOrderListFilters struct {
	Cursor        string
	Limit         int
	Status        string
	FromCurrency  string
	ToCurrency    string
	CreatedAfter  string
	CreatedBefore string
}

type Corridor struct {
	ID            string `json:"id"`
	Direction     string `json:"direction"`
	FromCurrency  string `json:"from_currency"`
	ToCurrency    string `json:"to_currency"`
	Country       string `json:"country"`
	PaymentMethod string `json:"payment_method"`
	Status        string `json:"status"`
	MinAmount     Money  `json:"min_amount"`
	MaxAmount     Money  `json:"max_amount"`
}

type CorridorListFilters struct {
	Direction  string
	Status     string
	Country    string
	ToCurrency string
}

type Currency struct {
	Code     string   `json:"code"`
	Type     string   `json:"type"`
	Decimals int      `json:"decimals"`
	Networks []string `json:"networks,omitempty"`
}

type CurrencyListFilters struct {
	Type string
}

type PaymentMethod struct {
	Code       string   `json:"code"`
	Label      string   `json:"label"`
	Directions []string `json:"directions"`
}

type PaymentMethodListFilters struct {
	Direction string
}

type Balance struct {
	Available   Money  `json:"available"`
	Currency    string `json:"currency"`
	CreditLimit Money  `json:"credit_limit"`
}

func (c *Client) do(method, path string, query url.Values, body any, idempotencyKey string, out any) error {
	fullURL := strings.TrimRight(c.BaseURL, "/") + path
	if len(query) > 0 {
		fullURL += "?" + query.Encode()
	}

	var reader io.Reader
	if body != nil {
		b, err := json.Marshal(body)
		if err != nil {
			return err
		}
		reader = bytes.NewReader(b)
	}

	req, err := http.NewRequest(method, fullURL, reader)
	if err != nil {
		return err
	}
	req.Header.Set("Authorization", "Bearer "+c.APIKey)
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	if idempotencyKey != "" {
		req.Header.Set("Idempotency-Key", idempotencyKey)
	}

	resp, err := c.HTTPClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return err
	}

	if resp.StatusCode >= 400 {
		var envelope errorEnvelope
		if jsonErr := json.Unmarshal(respBody, &envelope); jsonErr == nil && envelope.Error.Code != "" {
			return &envelope.Error
		}
		return &APIError{Code: "http_error", Message: fmt.Sprintf("unexpected status %d", resp.StatusCode)}
	}

	if out == nil || len(respBody) == 0 {
		return nil
	}
	return json.Unmarshal(respBody, out)
}

func (c *Client) CreatePayout(req CreatePayoutRequest, idempotencyKey string) (*Payout, error) {
	if idempotencyKey == "" {
		idempotencyKey = uuid.NewString()
	}
	var out Payout
	if err := c.do(http.MethodPost, "/payouts", nil, req, idempotencyKey, &out); err != nil {
		return nil, err
	}
	return &out, nil
}

func (c *Client) GetPayout(id string) (*Payout, error) {
	var out Payout
	if err := c.do(http.MethodGet, "/payouts/"+id, nil, nil, "", &out); err != nil {
		return nil, err
	}
	return &out, nil
}

func (c *Client) ListPayouts(filters PayoutListFilters) (*PayoutList, error) {
	q := url.Values{}
	if filters.Cursor != "" {
		q.Set("cursor", filters.Cursor)
	}
	if filters.Limit > 0 {
		q.Set("limit", strconv.Itoa(filters.Limit))
	}
	if filters.Status != "" {
		q.Set("status", filters.Status)
	}
	if filters.FromCurrency != "" {
		q.Set("from_currency", filters.FromCurrency)
	}
	if filters.ToCurrency != "" {
		q.Set("to_currency", filters.ToCurrency)
	}
	if filters.CreatedAfter != "" {
		q.Set("created_after", filters.CreatedAfter)
	}
	if filters.CreatedBefore != "" {
		q.Set("created_before", filters.CreatedBefore)
	}
	var out PayoutList
	if err := c.do(http.MethodGet, "/payouts", q, nil, "", &out); err != nil {
		return nil, err
	}
	return &out, nil
}

func (c *Client) CancelPayout(id string) (*Payout, error) {
	var out Payout
	if err := c.do(http.MethodPost, "/payouts/"+id+"/cancel", nil, nil, "", &out); err != nil {
		return nil, err
	}
	return &out, nil
}

func (c *Client) CreateOnRampOrder(req CreateOnRampOrderRequest, idempotencyKey string) (*OnRampOrder, error) {
	if idempotencyKey == "" {
		idempotencyKey = uuid.NewString()
	}
	var out OnRampOrder
	if err := c.do(http.MethodPost, "/onramp-orders", nil, req, idempotencyKey, &out); err != nil {
		return nil, err
	}
	return &out, nil
}

func (c *Client) GetOnRampOrder(id string) (*OnRampOrder, error) {
	var out OnRampOrder
	if err := c.do(http.MethodGet, "/onramp-orders/"+id, nil, nil, "", &out); err != nil {
		return nil, err
	}
	return &out, nil
}

func (c *Client) ListOnRampOrders(filters OnRampOrderListFilters) (*OnRampOrderList, error) {
	q := url.Values{}
	if filters.Cursor != "" {
		q.Set("cursor", filters.Cursor)
	}
	if filters.Limit > 0 {
		q.Set("limit", strconv.Itoa(filters.Limit))
	}
	if filters.Status != "" {
		q.Set("status", filters.Status)
	}
	if filters.FromCurrency != "" {
		q.Set("from_currency", filters.FromCurrency)
	}
	if filters.ToCurrency != "" {
		q.Set("to_currency", filters.ToCurrency)
	}
	if filters.CreatedAfter != "" {
		q.Set("created_after", filters.CreatedAfter)
	}
	if filters.CreatedBefore != "" {
		q.Set("created_before", filters.CreatedBefore)
	}
	var out OnRampOrderList
	if err := c.do(http.MethodGet, "/onramp-orders", q, nil, "", &out); err != nil {
		return nil, err
	}
	return &out, nil
}

func (c *Client) CancelOnRampOrder(id string) (*OnRampOrder, error) {
	var out OnRampOrder
	if err := c.do(http.MethodPost, "/onramp-orders/"+id+"/cancel", nil, nil, "", &out); err != nil {
		return nil, err
	}
	return &out, nil
}

func (c *Client) ListCorridors(filters CorridorListFilters) ([]Corridor, error) {
	q := url.Values{}
	if filters.Direction != "" {
		q.Set("direction", filters.Direction)
	}
	if filters.Status != "" {
		q.Set("status", filters.Status)
	}
	if filters.Country != "" {
		q.Set("country", filters.Country)
	}
	if filters.ToCurrency != "" {
		q.Set("to_currency", filters.ToCurrency)
	}
	var out struct {
		Data []Corridor `json:"data"`
	}
	if err := c.do(http.MethodGet, "/corridors", q, nil, "", &out); err != nil {
		return nil, err
	}
	return out.Data, nil
}

func (c *Client) ListCurrencies(filters CurrencyListFilters) ([]Currency, error) {
	q := url.Values{}
	if filters.Type != "" {
		q.Set("type", filters.Type)
	}
	var out struct {
		Data []Currency `json:"data"`
	}
	if err := c.do(http.MethodGet, "/currencies", q, nil, "", &out); err != nil {
		return nil, err
	}
	return out.Data, nil
}

func (c *Client) ListPaymentMethods(filters PaymentMethodListFilters) ([]PaymentMethod, error) {
	q := url.Values{}
	if filters.Direction != "" {
		q.Set("direction", filters.Direction)
	}
	var out struct {
		Data []PaymentMethod `json:"data"`
	}
	if err := c.do(http.MethodGet, "/payment-methods", q, nil, "", &out); err != nil {
		return nil, err
	}
	return out.Data, nil
}

func (c *Client) GetBalance() (*Balance, error) {
	var out Balance
	if err := c.do(http.MethodGet, "/balance", nil, nil, "", &out); err != nil {
		return nil, err
	}
	return &out, nil
}

func VerifyWebhookSignature(payload []byte, signatureHeader, secret string) bool {
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write(payload)
	expected := hex.EncodeToString(mac.Sum(nil))
	return hmac.Equal([]byte(expected), []byte(signatureHeader))
}
package main

import (
	"fmt"
	"os"

	// The package above is copied into your own module (it's a reference
	// client, not something you `go get` from Krypa) — replace this import
	// path with wherever you actually put it, e.g. "yourmodule/internal/krypa".
	"github.com/your-org/krypa-go/krypa"
)

func main() {
	client := krypa.NewClient(os.Getenv("KRYPA_API_KEY"))

	payout, err := client.CreatePayout(krypa.CreatePayoutRequest{
		ExternalID:    "order-4471",
		FromCurrency:  "USDT",
		FromAmount:    "500.00",
		ToCurrency:    "RUB",
		PaymentMethod: "sbp",
		Recipient: krypa.Recipient{
			Type:    "phone_bank",
			Phone:   "+79991234567",
			BankBIK: "044525225",
		},
	}, "")
	if err != nil {
		panic(err)
	}

	fmt.Printf("%s: %s\n", payout.ID, payout.Status)
}

Errors

Every non-2xx response is turned into a typed exception/error carrying the API's error.code and error.message verbatim — check code against the catalog in Errors rather than branching on the HTTP status alone, since several distinct failure modes (e.g. not_cancellable vs. idempotency_conflict) both return 409.

Idempotency

createPayout and createOnRampOrder are the only two endpoints that take an Idempotency-Key, matching the API — cancel endpoints don't accept one. Every client auto-generates a UUID v4 for you if you don't pass one explicitly, so a bare createPayout(req) call is always safe to retry on a network error without risking a duplicate.

Webhook verification

verifyWebhookSignature in each client implements exactly the check described in Webhooks: an HMAC-SHA256 of the raw request body, hex-encoded, compared against the X-Krypa-Signature header using a constant-time comparison. Pass it the untouched bytes of the request body — not a re-serialized copy of the parsed JSON — or verification will fail even for genuine events.

Sandbox

Build and test your integration with no real money at risk.

Sandbox is a fully separate environment, selected by using a sandbox API key. It shares the same API surface and the same corridor list as live, but:

  • Payouts settle in seconds against simulated routes, not real ones.
  • No real money moves, ever — there's no way to accidentally send a live payout with a sandbox key or vice versa.
  • Your sandbox balance is a large fixed test amount, not tied to any real prefunding.

Environment is per-request, not per-account

Every partner has both a sandbox and a live key. Which one you send in Authorization: Bearer … determines the environment for that specific request — there's nothing to toggle on your account globally.

Simulating outcomes

Sandbox payouts resolve deterministically based on the recipient details you send, so you can exercise every terminal state on demand:

Recipient valueResulting status
Any normal-looking valuecompleted after a few seconds
Phone number ending in 0000failed, with a realistic failure_reason
Amount ending in .13expired

Starts empty, by design

A freshly provisioned sandbox has no payouts in it — nothing is pre-generated for you. Everything you see there is something you actually created by calling the API, so it's safe to treat as real evidence of how your integration behaves, not a demo. History persists across sessions and is scoped to your partner account; there's no reset button, since there's nothing to clear that you didn't put there yourself.

Going live

Once your integration handles all six payout statuses and verifies webhook signatures, switch to a live key. Nothing else about the integration changes — same request/response shapes, same corridors, same webhook payloads.

Errors

The error response shape and the full code catalog.

Every error response has the same shape:

{
  "error": {
    "code": "corridor_unavailable",
    "message": "No active route for USDT → NGN via mobile_money right now.",
    "details": null
  }
}

error.code is stable and safe to match on programmatically. error.message is for humans (logs, support tickets) — don't parse it.

HTTP-level errors

These map directly to the responses documented in the API reference.

HTTP statuserror.codeMeaning
400validation_errorThe request body failed schema validation.
401unauthorizedMissing, revoked, or malformed API key.
402insufficient_balanceAvailable balance + credit limit can't cover this payout plus fees.
403insufficient_scopeThe key is read_only and this request creates or cancels something. Issue a read_write key instead.
404not_foundNo resource with that ID.
409idempotency_conflictThe Idempotency-Key was already used with a different request body.
409not_cancellablePayout is no longer pending (already processing or terminal).
422corridor_unavailableNo active route for this corridor right now.
429rate_limitedToo many requests — see the Retry-After header.

Payout failure_reason codes

Populated only when a payout's status is failed. These describe why the route failed, as opposed to why the API request was rejected.

failure_reasonMeaningTypically retryable?
recipient_bank_rejectedThe destination bank declined the transfer (bad account state, blocked recipient, etc).No — verify recipient details.
recipient_details_invalidRecipient details passed request validation but the route couldn't resolve them (e.g. unregistered phone number).No — verify recipient details.
route_timeoutThe route didn't confirm within its SLA.Yes — create a new payout.
insufficient_liquidityThe route ran out of local balance mid-settlement.Yes — Krypa's routing engine avoids this route until it recovers.
compliance_holdFlagged by automated compliance checks.Contact support.

Retrying a failed payout

Payouts are immutable once terminal — there's no "retry" endpoint. Create a new payout with a new external_id and Idempotency-Key.

Changelog

What changed, most recent first.

2026-09-03

  • Partner dashboard: added currency and country breakdowns to the Overview page.
  • Docs: published this site, including the full API reference generated from the OpenAPI contract.

2026-08-28

  • ON_RAMP API launched: POST /onramp-orders, GET /onramp-orders, GET /onramp-orders/{id}, POST /onramp-orders/{id}/cancel, and the onramp_order.updated webhook. Same auth, idempotency, and signing model as the existing payouts (OFF_RAMP) API — see How it works for both state machines.

2026-08-18

  • API: GET /corridors now supports filtering by country.
  • Added Kaspi transfer and Humo/Uzcard corridors (expanding) for KZ and UZ on-ramp.

2026-07-30

  • API: webhook deliveries now retry up to 6 times with exponential backoff, up from 3.
  • Dashboard: API keys can now be scoped read_only in addition to read_write.

2026-07-01

  • Payouts API reaches v1.0 general availability after nineteen months in private beta — POST /payouts, GET /payouts, GET /payouts/{id}, POST /payouts/{id}/cancel, GET /balance are now stable and covered by the deprecation policy below.

2026-06-12

  • Dashboard: added the Balance page (available + credit limit).

2026-05-08

  • Added Georgian lari corridors — TBC/BOG bank transfer (live) and card (expanding).

2026-04-02

  • Added UAE bank transfer corridor (live).

2026-03-11

  • Added Turkish lira corridors — Havale/EFT and Papara.

2026-02-19

  • Nigerian naira bank transfer corridor reaches live (mobile money remains expanding).

2026-02-05

  • Sandbox: published the deterministic outcome rules (magic phone/amount suffixes) so integrations can exercise every terminal status on demand instead of waiting on random timing.

2026-01-14

  • Security: webhook payloads are now signed with HMAC-SHA256 (X-Krypa-Signature) — verification is required starting this release.

2025-12-03

  • Added Nigerian naira and Kenyan shilling corridors (mobile money).

2025-11-06

  • Added Brazilian real corridor via PIX.

2025-10-09

  • API: added per-key rate limiting to protect shared infrastructure during the beta's growing traffic.

2025-09-04

  • Added Indian rupee corridors — UPI and IMPS.

2025-08-07

  • Added British pound corridor via Faster Payments.

2025-07-10

  • Dashboard: partners can now create and revoke their own API keys (previously issued manually by the team).

2025-06-05

  • Dashboard: first version shipped — transaction list and basic account overview.

2025-05-08

  • Added Eurozone corridor via SEPA.

2025-04-02

  • API: Idempotency-Key support added to POST /payouts — safe retries without double-paying a user.

2025-03-06

  • Webhooks introduced: payout.updated fires on every status transition, delivered to a per-partner configurable endpoint.

2025-02-04

  • Added Kazakhstani tenge corridor via Kaspi transfer.

2025-01-08

  • Sandbox environment opened for the first private beta partners.

2024-12-02

  • Private beta kicks off with a single corridor: USDT → RUB via SBP.