API reference
Audit
Tamper-evident audit trail: the HTTP request log, per-tenant event hash chains, signed daily anchors in write-once storage, hourly verification, signed export bundles and unified timelines.
GET /v1/admin/accounts/{id}/timeline
Timeline of a PSP or merchant account
The account's onboarding, configuration, credential and (as they are added) health events, oldest first; transactions are excluded. Requires accounts:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string | Account id. |
Responses
- 200 The timeline.
- 404 No such account (
not_found).
Request
curl -X GET "$KULPAY_URL/v1/admin/accounts/{id}/timeline" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"account_id": "3KWPsUwfH5am7QWL78hG1eA5hF8",
"items": [
{
"actor": "system",
"aggregate_id": "3KWPsVR76rXSyH4FUHif8mucXKO",
"at": "2026-10-10T22:15:57.9868Z",
"detail": {
"event_id": "3KWPsZjIhtYCYwYU9hJ9PF2fYXp",
"payload": {
"after": {
"account_id": "3KWPsUwfH5am7QWL78hG1eA5hF8",
"id": "3KWPsVR76rXSyH4FUHif8mucXKO",
"last4": "u7jc",
"mode": "test",
"name": "integration",
"scopes": [
"payments:read"
]
},
"changed": [
"account_id",
"id"
]
},
"schema_version": 1,
"sequence": 1
},
"direction": "internal",
"id": "3KWPsZjIhtYCYwYU9hJ9PF2fYXp",
"kind": "event",
"outcome": "progressed",
"summary": "api_key credential.api_key.created",
"type": "credential.api_key.created"
}
]
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "account not found",
"error": {
"code": "not_found",
"message": "account not found"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
GET /v1/admin/audit
Read the audit feed
Newest-first feed of every state-changing request. Each entry carries key_id, action, target, ip, body_hash, status, outcome, prev_hash, hash. Cap with ?limit=N (default 100, max 1000).
Parameters
| Name | In | Type | Description |
|---|---|---|---|
limit | query | integer |
Responses
- 200 Audit entries.
Request
curl -X GET "$KULPAY_URL/v1/admin/audit" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"count": 2,
"entries": [
{
"account_id": "3Kg6UFQm6mLoKocUxwifotF73Ob",
"action": "payment.refund",
"actor": "key:3KIhzpLdNupTuLgu8bwZOQspar2",
"at": "2026-10-11T10:12:44Z",
"body_hash": "e454a9599172d285c27e688ef7618a5a2e06295d8e30336299cb46a54391d5a7",
"hash": "57d24fde044989614455651b64b658e1b8592b1abd7e9154a747a82954685876",
"id": "pay_d0bb914e4510893d3ef98308",
"ip": "196.28.240.17",
"key_id": "3KIhzpLdNupTuLgu8bwZOQspar2",
"method": "POST",
"outcome": "succeeded",
"path": "/v1/payments/3KjLY7BV6lTzyJvGXr3mZ6e2cY8/refund",
"prev_hash": "a4708e0610200009884b5e2dcda4da8199bfda23637532b7816d01a9df12b7ef",
"request_id": "req_0ec9edbeb109266ce7ef1836",
"status": 200,
"target": "3KjLY7BV6lTzyJvGXr3mZ6e2cY8",
"user_agent": "kulpay-node/2.4.0"
},
{
"account_id": "3Kg6UFQm6mLoKocUxwifotF73Ob",
"action": "post.v1/webhook-endpoints",
"actor": "key:3KIhzpLdNupTuLgu8bwZOQspar2",
"at": "2026-10-11T09:58:02Z",
"body_hash": "01fda5546683ecf0ee16789b031e4f685554be92cd9c589d42d2ecc088bafaa0",
"hash": "a4708e0610200009884b5e2dcda4da8199bfda23637532b7816d01a9df12b7ef",
"id": "pay_e988f4b30c3835d77caa505e",
"ip": "196.28.240.17",
"key_id": "3KIhzpLdNupTuLgu8bwZOQspar2",
"method": "POST",
"outcome": "succeeded",
"path": "/v1/webhook-endpoints",
"prev_hash": "67e81257b728d54b76d9d1ac4fd9e03bb966cb5a8acd8f23140503bd8cfb39fc",
"status": 201,
"user_agent": "kulpay-node/2.4.0"
}
],
"next_cursor": "pay_d0bb914e4510893d3ef98308"
}
GET /v1/admin/audit/anchors
Signed daily anchors
Newest first. Requires audit:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
from | query | string (date) | |
to | query | string (date) | |
limit | query | integer |
Responses
- 200 The anchors.
- 400 Bad date or limit (
invalid_request).
Request
curl -X GET "$KULPAY_URL/v1/admin/audit/anchors" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"data": [
{
"chains": 2,
"created_at": "2026-10-10T22:15:59.199073083Z",
"day": "2026-10-10",
"document": {
"day": "2026-10-10",
"generated_at": "2026-10-10T22:15:59.198Z",
"heads": [
{
"chain_id": "tenant:3KWPsaD07P4cSnTR7riibX6S5GL",
"last_hash": "ab2e21369a7b9999bc9218e1106e047fb2663e84d09a0a702561cdd930771095",
"last_seq": 22
},
{
"chain_id": "tenant:3KWPsad9jYn6OMl14xjRYpHoPI2",
"last_hash": "8454fb004b4a911f5a6381ea0c641eb2269980edf5cf2b6f1eb2e5fdee4091d5",
"last_seq": 11
}
],
"key_id": "audit-test",
"merkle_root": "724afdde9b4b8d18d18253251e741ff49b2af7818d6cc95f9c0984b720126577",
"region": "mz",
"version": 1
},
"id": "3KWPsi0wdFm856395f81eDLYC1O",
"key_id": "audit-test",
"merkle_root": "724afdde9b4b8d18d18253251e741ff49b2af7818d6cc95f9c0984b720126577",
"public_key": "ANBaHR6iUTltVXr71FiLPG2Z2+uXL+0QoyVi6ibc3Po=",
"signature": "BH5wHDQIq8PocAswM+/fh0wso1EWnInEZ/jSWplUdWtii667KNhkuivk8ZEjJj875kReS2qAK/wK7WCc72+lAw==",
"signed": true
}
]
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "from and to are dates (YYYY-MM-DD)",
"error": {
"code": "invalid_request",
"message": "from and to are dates (YYYY-MM-DD)"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
GET /v1/admin/audit/chains
Audit chains and their verification state
Every chain's head and the result of the latest hourly verification. Requires audit:read.
Responses
- 200 The chains.
Request
curl -X GET "$KULPAY_URL/v1/admin/audit/chains" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"broken": 0,
"data": [
{
"chain_id": "http",
"last_hash": "ec6955a4fc6f1a88c64ae3f15026d1f0eef653c392e98f5ed39fb1fbfbca69b9",
"last_run_at": "2026-10-10T22:15:59.217462565Z",
"last_seq": 1,
"status": "ok",
"tenant_id": "",
"updated_at": "2026-10-10T22:15:59.324259818Z",
"verified_seq": 0
},
{
"chain_id": "tenant:3KWPsaD07P4cSnTR7riibX6S5GL",
"last_hash": "ab2e21369a7b9999bc9218e1106e047fb2663e84d09a0a702561cdd930771095",
"last_run_at": "2026-10-10T22:15:59.217462565Z",
"last_seq": 22,
"status": "ok",
"tenant_id": "3KWPsaD07P4cSnTR7riibX6S5GL",
"updated_at": "2026-10-10T22:15:59.15848143Z",
"verified_seq": 22
}
],
"unchained_events": 2
}
POST /v1/admin/audit/chains/verify
Verify the chains now
Runs one pass of the hourly verification: every event added since the last verified position, its link to the previous one, and agreement with the latest anchor. A break emits audit.chain.broken and raises a critical alert. Requires admin:write.
Responses
- 200 The pass.
- 503 The verifier is not running (
audit_unavailable).
Request
curl -X POST "$KULPAY_URL/v1/admin/audit/chains/verify" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"broken": 0,
"chains": 3,
"events_checked": 33
}
503 Error · audit_unavailable application/problem+json
{
"code": "audit_unavailable",
"detail": "The verifier is not running (audit_unavailable).",
"error": {
"code": "audit_unavailable",
"message": "The verifier is not running (audit_unavailable)."
},
"status": 503,
"title": "Service Unavailable",
"type": "about:blank"
}
POST /v1/admin/audit/exports
Export a signed audit bundle
Produces a ZIP for one tenant and period: events.jsonl and events.csv (the tenant's chain, contiguous), http_audit.jsonl, anchors.json, manifest.json (SHA-256 of every file) and manifest.sig (Ed25519). Verify it offline with kulpay-payments audit verify bundle.zip --public-key <key>. The export is recorded as audit.export.created on the tenant's chain. At most 92 days per bundle. Requires audit:read.
Request body application/json required
| Field | Type | Description |
|---|---|---|
from required | string | RFC 3339 time or YYYY-MM-DD (inclusive). |
tenant_id required | string | Account id, or `platform`. |
to required | string | RFC 3339 time or YYYY-MM-DD (exclusive). |
Responses
- 200 The bundle.
X-Audit-Export-IdandX-Audit-Eventsdescribe it. - 400 Missing tenant, bad or inverted range (
invalid_request), or more than 92 days (range_too_large). - 404 No such tenant (
not_found). - 503 Exports are not enabled (
audit_unavailable).
Request
curl -X POST "$KULPAY_URL/v1/admin/audit/exports" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "2026-10-10T21:15:58Z",
"tenant_id": "3KWPsfhQUTnXIQuAYdECfQ9TDyp",
"to": "2026-10-10T23:15:58Z"
}'
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "to must be after from",
"error": {
"code": "invalid_request",
"message": "to must be after from"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "tenant not found",
"error": {
"code": "not_found",
"message": "tenant not found"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
503 Error · audit_unavailable application/problem+json
{
"code": "audit_unavailable",
"detail": "Exports are not enabled (audit_unavailable).",
"error": {
"code": "audit_unavailable",
"message": "Exports are not enabled (audit_unavailable)."
},
"status": 503,
"title": "Service Unavailable",
"type": "about:blank"
}
GET /v1/admin/audit/public-key
The audit signing key
The Ed25519 public key that signs anchors and export bundles. Pin it out of band and pass it to kulpay-payments audit verify --public-key. Requires audit:read.
Responses
- 200 The key.
- 404 No signing key is configured (
audit_unsigned).
Request
curl -X GET "$KULPAY_URL/v1/admin/audit/public-key" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"algorithm": "ed25519",
"key_id": "audit-test",
"public_key": "ANBaHR6iUTltVXr71FiLPG2Z2+uXL+0QoyVi6ibc3Po="
}
404 Error · audit_unsigned application/problem+json
{
"code": "audit_unsigned",
"detail": "No signing key is configured (audit_unsigned).",
"error": {
"code": "audit_unsigned",
"message": "No signing key is configured (audit_unsigned)."
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
GET /v1/admin/audit/verify
Verify the audit hash chain
Walks every audit entry chronologically and confirms that each entry's hash equals sha256(canonical(entry)) AND each prev_hash equals the previous entry's hash. Returns 200 when the chain is intact, 409 with broken_at and reason otherwise.
Responses
- 200 Chain intact.
- 409 Tampering detected.
Request
curl -X GET "$KULPAY_URL/v1/admin/audit/verify" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"chain_head": "",
"length": 0,
"verified": true
}
409 Error application/json
{
"broken_at": 1842,
"chain_head": "57d24fde044989614455651b64b658e1b8592b1abd7e9154a747a82954685876",
"reason": "hash mismatch at position 1842",
"verified": false
}
GET /v1/payments/{id}/audit
Read the audit feed for one payment
Same shape as /v1/admin/audit filtered to entries whose target matches the path parameter.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string | |
limit | query | integer |
Responses
- 200 Audit entries for this payment.
Request
curl -X GET "$KULPAY_URL/v1/payments/{id}/audit" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"count": 2,
"entries": [
{
"account_id": "3Kg6UFQm6mLoKocUxwifotF73Ob",
"action": "payment.refund",
"actor": "key:3KIhzpLdNupTuLgu8bwZOQspar2",
"at": "2026-10-11T10:12:44Z",
"body_hash": "e454a9599172d285c27e688ef7618a5a2e06295d8e30336299cb46a54391d5a7",
"hash": "57d24fde044989614455651b64b658e1b8592b1abd7e9154a747a82954685876",
"id": "pay_d0bb914e4510893d3ef98308",
"ip": "196.28.240.17",
"key_id": "3KIhzpLdNupTuLgu8bwZOQspar2",
"method": "POST",
"outcome": "succeeded",
"path": "/v1/payments/3KjLY7BV6lTzyJvGXr3mZ6e2cY8/refund",
"prev_hash": "a4708e0610200009884b5e2dcda4da8199bfda23637532b7816d01a9df12b7ef",
"request_id": "req_0ec9edbeb109266ce7ef1836",
"status": 200,
"target": "3KjLY7BV6lTzyJvGXr3mZ6e2cY8",
"user_agent": "kulpay-node/2.4.0"
},
{
"account_id": "3Kg6UFQm6mLoKocUxwifotF73Ob",
"action": "payment.capture",
"actor": "key:3KIhzpLdNupTuLgu8bwZOQspar2",
"at": "2026-10-08T14:21:12Z",
"body_hash": "fd8747f4bca05336bbe57747d664100e9e6123cec386f8249e9bd0db0343a15c",
"hash": "67e81257b728d54b76d9d1ac4fd9e03bb966cb5a8acd8f23140503bd8cfb39fc",
"id": "pay_be87706e93eb60fa46fb7d8b",
"ip": "196.28.240.17",
"key_id": "3KIhzpLdNupTuLgu8bwZOQspar2",
"method": "POST",
"outcome": "succeeded",
"path": "/v1/payments/3KjLY7BV6lTzyJvGXr3mZ6e2cY8/capture",
"prev_hash": "127eb02825bf27f39d3330cb11c7f12aa587e25bf0582310d44c10c8163fb90b",
"status": 200,
"target": "3KjLY7BV6lTzyJvGXr3mZ6e2cY8",
"user_agent": "kulpay-node/2.4.0"
}
],
"next_cursor": "pay_d0bb914e4510893d3ef98308"
}
GET /v1/transactions/{id}/stages
Processing stages of a transaction
Each processing stage of one payment, in the order it ran, with started_at, ended_at, duration_ms, outcome, reason_code, the PSP's raw_code, KulPay's mapped_code, the attempt number and the psp that handled it: where the transaction stopped and why. Each stage also emits transaction.stage.<stage>.<outcome>. Stages and their outcomes: receive: accepted; enrich_validate: passed; risk: skipped, approved, review_required, declined, challenged; route: routed, no_route; submit: accepted, requires_action, declined, soft_declined, error, unknown; await_confirmation: succeeded, failed, cancelled, expired; post: posted; notify: queued, skipped; settle: settled; reconcile: matched, exception. A payment of another account is reported as not found. Requires transactions:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string | Payment id. |
Responses
- 200 The stages.
- 403 Missing
transactions:read(insufficient_scope). - 404 No such payment for this account.
not_found
Request
curl -X GET "$KULPAY_URL/v1/transactions/{id}/stages" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"data": [
{
"attempt": 1,
"duration_ms": 0,
"ended_at": "2026-10-10T22:15:45.71626081Z",
"id": "3KWPqx53WxvpSwvNu2c5fSulwx3",
"outcome": "accepted",
"payment_id": "3KWPqy64mjpEmk7YbiRaVXD1OFo",
"stage": "receive",
"started_at": "2026-10-10T22:15:45.71626081Z",
"transaction_type": "SALE"
},
{
"attempt": 1,
"duration_ms": 0,
"ended_at": "2026-10-10T22:15:45.71696009Z",
"id": "3KWPr3O87yjos0HPrldcBjR2kZG",
"outcome": "passed",
"payment_id": "3KWPqy64mjpEmk7YbiRaVXD1OFo",
"stage": "enrich_validate",
"started_at": "2026-10-10T22:15:45.71626081Z",
"transaction_type": "SALE"
}
],
"total": 8
}
403 Error · insufficient_scope application/problem+json
{
"code": "insufficient_scope",
"detail": "missing required scope: transactions:read",
"error": {
"code": "insufficient_scope",
"message": "missing required scope: transactions:read",
"missing_scope": "transactions:read"
},
"missing_scope": "transactions:read",
"status": 403,
"title": "Forbidden",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "transaction not found",
"error": {
"code": "not_found",
"message": "transaction not found"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
GET /v1/transactions/{id}/timeline
Unified timeline of a transaction
Every record about one payment in one ordered list: its events and its refunds' events, events recorded by the same requests, the requests themselves, provider callbacks, webhook deliveries and ledger postings. A payment of another account is reported as not found. Requires transactions:read. A cash deposit's id (its intent, the transaction the partner sees) returns its journey instead (IO-7.1): {intent_id, lookup_id, correlation_id, deposit_id, status, outcome, final, items[]}, each item {at, stage, type, aggregate_type, aggregate_id, data} in order: lookup (with the routing rule), intent, confirmation, credit, application at the resolver, suspense, reconciliation, outcome. The correlation_id is the lookup ID KulPay sends the resolver as X-Correlation-ID on every call.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string | Payment id. |
direction | query | string | inbound, outbound or internal (PSP-7.1). One of: inbound, outbound, internal |
pattern | query | string | The interaction pattern (PSP-7.1). One of: outbound_sync, outbound_async, outbound_notification, inbound_request, inbound_callback, inbound_file, outbound_file, status_query |
Responses
- 200 The timeline.
- 403 Missing
transactions:read(insufficient_scope). - 404 No such transaction (
not_found). - 503 No payments store (
timeline_unavailable).
Request
curl -X GET "$KULPAY_URL/v1/transactions/{id}/timeline" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"account_id": "3KWPrmSWzqQXwxG1hY9szJPTTrB",
"counts": {
"event": 15,
"ledger": 2,
"request": 1
},
"items": [
{
"actor": "system",
"aggregate_id": "3KWPriUVlsoE66gvbeypObiNyg4",
"at": "2026-10-10T22:15:51.590718Z",
"detail": {
"event_id": "3KWPrluQuwW6HHzKcNJvGTNaCx1",
"payload": {
"account_id": "3KWPrmSWzqQXwxG1hY9szJPTTrB",
"amount": "10",
"amount_minor": 1000,
"amount_refunded": "0",
"created_at": "2026-10-10T22:15:51.589301639Z",
"currency": "MZN",
"id": "3KWPriUVlsoE66gvbeypObiNyg4",
"method": "card",
"mode": "test",
"status": "created"
},
"schema_version": 2,
"sequence": 1
},
"direction": "internal",
"id": "3KWPrluQuwW6HHzKcNJvGTNaCx1",
"kind": "event",
"outcome": "progressed",
"summary": "payment created",
"type": "payment.created"
},
{
"actor": "system",
"aggregate_id": "3KWPriUVlsoE66gvbeypObiNyg4",
"at": "2026-10-10T22:15:51.592091Z",
"detail": {
"event_id": "3KWPriUJUj7yaEBOuGvHzvOz9pH",
"payload": {
"attempt": 1,
"duration_ms": 0,
"ended_at": "2026-10-10T22:15:51.571983101Z",
"id": "3KWPrmtasVjb4eSCE0ZPxh5pU0A",
"outcome": "accepted",
"payment_id": "3KWPriUVlsoE66gvbeypObiNyg4",
"stage": "receive",
"started_at": "2026-10-10T22:15:51.571983101Z",
"transaction_type": "SALE"
},
"schema_version": 1,
"sequence": 2
},
"direction": "internal",
"duration_ms": 1,
"id": "3KWPriUJUj7yaEBOuGvHzvOz9pH",
"kind": "event",
"outcome": "progressed",
"summary": "payment transaction.stage.receive.accepted",
"type": "transaction.stage.receive.accepted"
}
],
"transaction_id": "3KWPriUVlsoE66gvbeypObiNyg4",
"truncated": false
}
403 Error · insufficient_scope application/problem+json
{
"code": "insufficient_scope",
"detail": "missing required scope: transactions:read",
"error": {
"code": "insufficient_scope",
"message": "missing required scope: transactions:read",
"missing_scope": "transactions:read"
},
"missing_scope": "transactions:read",
"status": 403,
"title": "Forbidden",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "transaction not found",
"error": {
"code": "not_found",
"message": "transaction not found"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
503 Error · timeline_unavailable application/problem+json
{
"code": "timeline_unavailable",
"detail": "No payments store (timeline_unavailable).",
"error": {
"code": "timeline_unavailable",
"message": "No payments store (timeline_unavailable)."
},
"status": 503,
"title": "Service Unavailable",
"type": "about:blank"
}
KulPay