OpenAPI 3.1
The full spec, ready to import into Postman or Insomnia. Updated on every release.
Download openapi.yamlREST endpoints under /v1/. UTF-8 JSON responses. OAuth 2.0 Bearer authentication. Cursor pagination. Full OpenAPI 3.1 spec available for download.
The full spec, ready to import into Postman or Insomnia. Updated on every release.
The full spec, ready to import into Postman or Insomnia. Updated on every release.
Download openapi.yamlAll requests require a Bearer access token. Exchange your client keys (sk_test_ or sk_live_ prefix) for a short-lived token via /v1/auth/token. Scopes are explicit: kyc:read, kyc:write, orders:write, audit:read, webhooks:manage.
Rotation: secrets can be rotated without downtime via the dashboard. The old secret remains valid 24 h after a new one is issued.
# 1. Exchange client credentials for an access token
curl -X POST https://api.getinopay.com/v1/auth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "client_credentials",
"client_id": "ino_client_8XK9R2",
"client_secret": "sk_live_4bX9...PqW2",
"scope": "kyc:read orders:write audit:read"
}'
# Response 200 OK
# {
# "access_token": "eyJhbGciOiJFZERTQSIs...",
# "token_type": "Bearer",
# "expires_in": 900,
# "scope": "kyc:read orders:write audit:read"
# }
# 2. Authenticated calls
curl -X GET https://api.getinopay.com/v1/orders/ord_9Pk2X \
-H "Authorization: Bearer eyJhbGciOiJFZERTQSIs..."import os
import requests
# Exchange credentials for a Bearer token
resp = requests.post(
"https://api.getinopay.com/v1/auth/token",
json={
"grant_type": "client_credentials",
"client_id": os.environ["INOPAY_CLIENT_ID"],
"client_secret": os.environ["INOPAY_CLIENT_SECRET"],
"scope": "kyc:read orders:write",
},
timeout=5,
)
resp.raise_for_status()
token = resp.json()["access_token"]
# Use the Bearer for subsequent calls
order = requests.get(
"https://api.getinopay.com/v1/orders/ord_9Pk2X",
headers={"Authorization": f"Bearer {token}"},
timeout=5,
).json()// Node 20+ / Browser fetch
const tokenRes = await fetch('https://api.getinopay.com/v1/auth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'client_credentials',
client_id: process.env.INOPAY_CLIENT_ID,
client_secret: process.env.INOPAY_CLIENT_SECRET,
scope: 'kyc:read orders:write',
}),
});
const { access_token } = await tokenRes.json();
const orderRes = await fetch(
'https://api.getinopay.com/v1/orders/ord_9Pk2X',
{ headers: { Authorization: `Bearer ${access_token}` } },
);
const order = await orderRes.json();UTF-8 JSON bodies. ISO 8601 dates (e.g. 2026-04-25T14:32:00Z, always UTC). Amounts in XOF cents (integers). Pseudonymous identifiers (ino_xxx, ord_xxx, att_xxx).
# Example POST body — amounts in XOF cents (entiers)
{
"rcpt_to": "sgi_partner_001",
"kyc_attestation_id": "att_4XK9RZ",
"instrument": "SNTS.BRVM",
"side": "buy",
"qty": 10,
"limit_price_cents": 1250000, // 12 500 XOF
"submitted_at": "2026-04-25T14:32:00Z",
"client_metadata": {
"internal_ref": "BO-2026-04-25-0001"
}
}
# Example successful response (201 Created)
{
"id": "ord_9Pk2X",
"status": "routed",
"rcpt_to": "sgi_partner_001",
"instrument": "SNTS.BRVM",
"side": "buy",
"qty": 10,
"limit_price_cents": 1250000,
"filled_qty": 0,
"average_fill_price_cents": null,
"created_at": "2026-04-25T14:32:00.142Z",
"updated_at": "2026-04-25T14:32:00.142Z"
}Idempotency-Key header mandatory on all mutating POSTs. UUID v4 recommended. Responses are cached for 24 h: a retry with the same key returns the original response (same HTTP code).
# Both POSTs produce one order — second returns the cached 201
curl -X POST https://api.getinopay.com/v1/orders \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: 6c4b0b6e-1c2a-4d09-9b3a-7d1f4e5a6c2d" \
-H "Content-Type: application/json" \
-d @order.json
# Retry within 24h with the same key:
curl -X POST https://api.getinopay.com/v1/orders \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: 6c4b0b6e-1c2a-4d09-9b3a-7d1f4e5a6c2d" \
-H "Content-Type: application/json" \
-d @order.json
# → 201 Created (same body, same id, replayed from cache)Opaque cursor pagination: pass ?cursor=<value>&limit=50 (max 200). The X-Cursor-Next header contains the next page cursor (empty when done).
# First page
GET /v1/orders?limit=50
# Response headers
HTTP/1.1 200 OK
X-Cursor-Next: eyJzIjoib3JkXzlQazJYIiwidCI6MTc2NDA4MjMyMH0
X-Cursor-Prev:
# Next page
GET /v1/orders?limit=50&cursor=eyJzIjoib3JkXzlQazJYIiwidCI6MTc2NDA4MjMyMH01000 req/min in sandbox, 10000 req/min in production (per client_id). Headers X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (Unix timestamp). Beyond: 429 Too Many Requests with Retry-After header.
# Headers returned on every API call
HTTP/1.1 200 OK
X-RateLimit-Limit: 10000
X-RateLimit-Remaining: 9847
X-RateLimit-Reset: 1764082380
# When exceeded
HTTP/1.1 429 Too Many Requests
Retry-After: 27
Content-Type: application/json
{
"code": "rate_limit_exceeded",
"message": "Rate limit reached. Retry in 27 seconds.",
"request_id": "req_a3f2b9c1"
}Route overview. Detailed schemas (parameters, error codes) live in the OpenAPI spec.
| Method | Path | Description | Scope |
|---|---|---|---|
| POST | /v1/auth/token | Exchange client keys → Bearer access token. | — |
| GET | /v1/kyc/attestations/{id} | Fetch an attestation and its status. | kyc:read |
| POST | /v1/kyc/attestations | Create an Ed25519-signed KYC attestation. | kyc:write |
| GET | /v1/sgi/partners | Public list of approved SGI partners. | public |
| POST | /v1/orders | Submit an order routed to an approved SGI (rcpt_to). | orders:write |
| GET | /v1/orders/{id} | Current order state, fills included. | orders:read |
| POST | /v1/webhooks/endpoints | Register an endpoint for event delivery. | webhooks:manage |
| GET | /v1/audit/snapshots/{date} | Fetch an anchored Merkle snapshot. | audit:read |
Standard HTTP codes. Normalized JSON body: { code, message, details, request_id }. Provide request_id when contacting support.
# 401 Unauthorized — invalid or expired token
{ "code": "unauthorized", "message": "Access token expired", "request_id": "req_a3f2b9c1" }
# 403 Forbidden — token lacks scope
{ "code": "forbidden", "message": "Missing scope: orders:write", "request_id": "req_a3f2b9c2" }
# 404 Not Found — unknown resource
{ "code": "not_found", "message": "Order ord_unknown not found", "request_id": "req_a3f2b9c3" }
# 422 Unprocessable Entity — validation failure
{
"code": "validation_error",
"message": "qty must be a positive integer",
"details": [{ "field": "qty", "rule": "min", "value": 0 }],
"request_id": "req_a3f2b9c4"
}
# 429 Too Many Requests — see Rate limiting above
# 500 Internal Server Error — transient; retry with same Idempotency-Key
{ "code": "internal_error", "message": "An unexpected error occurred", "request_id": "req_a3f2b9c5" }