KulPayAPI Live

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

NameInTypeDescription
id requiredpathstring 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

NameInTypeDescription
limitqueryinteger

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

NameInTypeDescription
fromquerystring (date)
toquerystring (date)
limitqueryinteger

Responses

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

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

FieldTypeDescription
from requiredstring RFC 3339 time or YYYY-MM-DD (inclusive).
tenant_id requiredstring Account id, or `platform`.
to requiredstring RFC 3339 time or YYYY-MM-DD (exclusive).

Responses

  • 200 The bundle. X-Audit-Export-Id and X-Audit-Events describe 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

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

NameInTypeDescription
id requiredpathstring
limitqueryinteger

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

NameInTypeDescription
id requiredpathstring Payment id.

Responses

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

NameInTypeDescription
id requiredpathstring Payment id.
directionquerystring inbound, outbound or internal (PSP-7.1).
One of: inbound, outbound, internal
patternquerystring 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

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"
}