KulPayAPI Live

API reference

PSP onboarding

A PSP applies, is reviewed, set up, certified and taken live through a recorded, approvable lifecycle.

POST /callbacks/{psp_id}/{binding_id}

A PSP callback to its own binding

Where a PSP sends its callbacks for one connector binding (PSP-7.3); the URL is shown with the binding (callback.url on /v1/admin/connectors and /admin/connectors). No API key: the callback is verified with the binding's own configuration, which is the driver's signature check under the tenant's credentials (default), hmac (X-KulPay-Signature: sha256=<hex HMAC-SHA256 of the body>), token (X-Callback-Token, or ?token=), certificate (a pinned client certificate, when KulPay terminates TLS), or none (test mode only), plus an optional IP allow-list. A verified callback is acknowledged at once and processed asynchronously with the tenant's connector, which settles only that tenant's payments. Events: callback.received, then callback.verified or callback.rejected.

Parameters

NameInTypeDescription
psp_id requiredpathstring The PSP's id, or the account's.
binding_id requiredpathstring

Request body application/json required

Responses

  • 200 {"received": true}: verified and queued (a retry of a queued callback answers the same).
  • 401 gateway.callback_rejected: failed verification; the raw request is kept for investigation. callback_rejected
  • 404 No such endpoint (unknown binding, or not this PSP's). not_found

Request

curl -X POST "$KULPAY_URL/callbacks/{psp_id}/{binding_id}" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
200 Response application/json
{
  "received": true
}
401 Error · callback_rejected application/problem+json
{
  "code": "callback_rejected",
  "detail": "The callback did not pass this binding's verification.",
  "error": {
    "code": "callback_rejected",
    "message": "The callback did not pass this binding's verification."
  },
  "status": 401,
  "title": "Callback rejected",
  "type": "about:blank"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "No such callback endpoint.",
  "error": {
    "code": "not_found",
    "message": "No such callback endpoint."
  },
  "status": 404,
  "title": "Not found",
  "type": "about:blank"
}

POST /callbacks/{psp_id}/{binding_id}/events

Receive an event a PSP notifies KulPay of

A PSP that notifies KulPay of its own changes (KulOffice's outbox, IO-0.5) posts each event here, signed under its binding (kulpay_cms: KulOffice-Signature t=<unix>,v1=<hex HMAC-SHA256 of "<t>.<body>">, at most 5 minutes old). Each event_id is processed once: a repeat is 200, nothing done. An aggregate's events apply in their sequence: one that comes early is held and KulPay reads the missing ones from the PSP (GET /v1/events?since=). An event that cannot be processed is dead (psp_event.dead_lettered) with its reason. A delivery that does not verify is 401 and a rejected callback (GET /v1/admin/callbacks).

Parameters

NameInTypeDescription
psp_id requiredpathstring The PSP's id, or the account's.
binding_id requiredpathstring

Request body application/json required

FieldTypeDescription
aggregate_id requiredstring
aggregate_type requiredstring
data requiredobject
event_id requiredstring
occurred_atstring (date-time)
schema_versioninteger
sequence requiredinteger
type requiredstring

Responses

Request

curl -X POST "$KULPAY_URL/callbacks/{psp_id}/{binding_id}/events" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "aggregate_id": "cr_3KhN7okcrh0RiJWq5q1gbfGJqKX",
  "aggregate_type": "credit",
  "data": {
    "credit_id": "cr_3KhN7okcrh0RiJWq5q1gbfGJqKX",
    "deposit_id": "3KhN7iNFfIs5YvAwTxaDRGyEpIN"
  },
  "event_id": "evt_2NbQ1x",
  "occurred_at": "2026-10-15T10:00:00Z",
  "schema_version": 1,
  "sequence": 1,
  "type": "wallet.credited"
}'
200 Response application/json
{
  "received": true,
  "status": "applied"
}
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "An event body of at most 1 MiB is required.",
  "error": {
    "code": "invalid_request",
    "message": "An event body of at most 1 MiB is required."
  },
  "status": 400,
  "title": "Invalid body",
  "type": "about:blank"
}
401 Error · callback_rejected application/problem+json
{
  "code": "callback_rejected",
  "detail": "The event did not pass this binding's verification.",
  "error": {
    "code": "callback_rejected",
    "message": "The event did not pass this binding's verification."
  },
  "status": 401,
  "title": "Callback rejected",
  "type": "about:blank"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "No such event endpoint.",
  "error": {
    "code": "not_found",
    "message": "No such event endpoint."
  },
  "status": 404,
  "title": "Not found",
  "type": "about:blank"
}

GET /v1/admin/approvals

Maker-checker requests

Parameters

NameInTypeDescription
subject_idquerystring
pendingquerystring

Responses

  • 200 Requests.

Request

curl -X GET "$KULPAY_URL/v1/admin/approvals" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "data": [
    {
      "created_at": "2026-10-10T16:15:22.005996Z",
      "decided_at": "2026-10-10T16:15:22.021089Z",
      "decided_by": "9c65c40c72be831e4987292492da6bfa",
      "decision_reason": "playground sandbox",
      "id": "3KVi1gXAMzy3tK5Mb3fibPDDsSL",
      "kind": "bill.biller_status",
      "payload": "{\"status\":\"active\"}",
      "prepared_by": "f5ed8d5b44ff381e41d910db872c64ac",
      "reason": "playground sandbox",
      "status": "approved",
      "subject_id": "3KVi1cUPS01yJkDC7pITrc8G4WQ",
      "tenant_id": "platform"
    },
    {
      "created_at": "2026-10-10T16:15:21.927802Z",
      "decided_at": "2026-10-10T16:15:21.943088Z",
      "decided_by": "9c65c40c72be831e4987292492da6bfa",
      "decision_reason": "playground sandbox",
      "id": "3KVi1aqsDaTfwtvqG8RGBmMf6LO",
      "kind": "bill.biller_status",
      "payload": "{\"status\":\"active\"}",
      "prepared_by": "f5ed8d5b44ff381e41d910db872c64ac",
      "reason": "playground sandbox",
      "status": "approved",
      "subject_id": "3KVi1arnE6Mzj1j3PjS3XKxQsdq",
      "tenant_id": "platform"
    }
  ]
}

POST /v1/admin/approvals/{id}/decide

Approve or reject a request

The checker must not be the person who prepared it. Approval applies the decision in the same transaction. Requires accounts:write.

Parameters

NameInTypeDescription
id requiredpathstring

Request body application/json required

FieldTypeDescription
approve requiredboolean
reasonstring

Responses

Request

curl -X POST "$KULPAY_URL/v1/admin/approvals/{id}/decide" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "approve": true,
  "reason": "ok"
}'
200 Response application/json
{
  "created_at": "2026-10-10T22:14:57.89256229Z",
  "decided_at": "2026-10-10T22:14:57.919880851Z",
  "decided_by": "key_checker",
  "decision_reason": "ok",
  "id": "3KWPkwtupluYYLGAw7At48TbMyG",
  "kind": "bill.biller_status",
  "payload": "{\"status\":\"active\"}",
  "prepared_by": "key_accounts",
  "reason": "contract",
  "status": "approved",
  "subject_id": "3KWPkylzKmRsF6Rq3xef8pjun2x",
  "tenant_id": "platform"
}
403 Error · same_approver application/problem+json
{
  "code": "same_approver",
  "detail": "payments: the person who prepared a request cannot approve it",
  "error": {
    "code": "same_approver",
    "message": "payments: the person who prepared a request cannot approve it"
  },
  "status": 403,
  "title": "Forbidden",
  "type": "about:blank"
}
409 Error · already_decided application/problem+json
{
  "code": "already_decided",
  "detail": "already_decided",
  "error": {
    "code": "already_decided",
    "message": "already_decided"
  },
  "status": 409,
  "title": "Conflict",
  "type": "about:blank"
}

GET /v1/admin/credit-reconciliations

The days of credits reconciled

The days resolvers reconciled (IO-6.2), of one resolver (psp_id) or all, of a date or all, newest first: KulPay's applied total, the resolver's ledger and settled totals, what is missing on each side, and whether the day closed. Requires reconciliation:read.

Parameters

NameInTypeDescription
psp_idquerystring
datequerystring

Responses

  • 200 The days.

Request

curl -X GET "$KULPAY_URL/v1/admin/credit-reconciliations" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "data": [
    {
      "applied_minor": 50000,
      "currency": "MZN",
      "date": "2026-10-14",
      "id": "3KXgREC00000000000000000001",
      "ledger_minor": 50000,
      "missing_at_kulpay": 0,
      "missing_at_resolver": 0,
      "mode": "live",
      "reported_at": "2026-10-15T02:00:00Z",
      "resolver_psp_id": "3KXgPSPkulOffice00000000001",
      "settled_minor": 50000,
      "status": "closed"
    }
  ]
}

GET /v1/admin/interop/dashboard

The interoperability at a glance

For KulPay's operators (IO-7.2), over the last hours (24 by default): reference lookups by resolver and routing rule, with their found, ambiguous and not_found counts and rate, average and p95 latency and whether the resolver's lookup circuit is open; the credits whose outcome is unknown or in review, the deposits in suspense, the open interop exceptions and the PSPs' events dead-lettered, each with its age; and the alerts: a resolver's circuit open, more than 1% of the last hour's credits with an unknown outcome, a biller event dead-lettered. The same screen is /admin/interop. Requires reconciliation:read.

Parameters

NameInTypeDescription
hoursqueryinteger

Responses

  • 200 The dashboard.

Request

curl -X GET "$KULPAY_URL/v1/admin/interop/dashboard" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "alerts": [],
  "credits": [],
  "dead_events": [],
  "exceptions": [],
  "lookups": [
    {
      "ambiguous": 2,
      "avg_latency_ms": 180,
      "circuit_open": false,
      "found": 110,
      "found_rate": 0.9167,
      "not_found": 8,
      "p95_latency_ms": 420,
      "psp_id": "3KXgPSPkulOffice00000000001",
      "routing_rule": "method_binding",
      "total": 120
    }
  ],
  "since": "2026-10-14T10:00:00Z",
  "suspended": [],
  "unknown_credit_rate_last_hour": 0
}

GET /v1/admin/psp-reviews

Review queue

Periodic reviews due within 30 days or overdue (high risk every 6 months, medium 12, low 24) and emergency suspensions waiting for a second officer (24 hours). An hourly job records psp.review.due and psp.suspension.review_overdue.

Responses

  • 200 The queue, most urgent first.

Request

curl -X GET "$KULPAY_URL/v1/admin/psp-reviews" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "data": [
    {
      "approval_id": "3KWPqBtjd4R5ETH8OB7th9QWOjF",
      "due_at": "2026-10-11T22:15:38.710738721Z",
      "kind": "emergency_suspension",
      "overdue": false,
      "psp": {
        "account_id": "3KWPqAm6bUbJrAb7hW8f2ZU6pPT",
        "application_id": "3KWPq6zQflNIw3ra10OHzdZABgk",
        "contract_signed": false,
        "country": "MZ",
        "created_at": "2026-10-10T22:15:38.521919662Z",
        "emergency_suspended_at": "2026-10-10T22:15:38.710738721Z",
        "id": "3KWPq84JWIgPA0bRlrInvqXVHZS",
        "kind": "psp",
        "kyb_status": "in_review",
        "language": "pt",
        "legal_name": "Banco Exemplo SA",
        "next_review_at": "2027-04-10T22:15:38.627684253Z",
        "region": "",
        "registration_number": "100234567",
        "resume_state": "technical_setup",
        "risk_tier": "high",
        "sandbox_enabled_at": "2026-10-10T22:15:38.605512701Z",
        "sandbox_key_id": "3KWPqBPUHHtpYIo3jN74t1OHRVj",
        "state_entered_at": "2026-10-10T22:15:38.709280961Z",
        "status": "suspended",
        "suspension_review_id": "3KWPqBtjd4R5ETH8OB7th9QWOjF",
        "updated_at": "2026-10-10T22:15:38.709280961Z"
      }
    }
  ]
}

GET /v1/admin/psps

List PSPs

Requires accounts:read.

Parameters

NameInTypeDescription
statusquerystring

Responses

  • 200 PSPs, newest first.

Request

curl -X GET "$KULPAY_URL/v1/admin/psps" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "data": [
    {
      "account_id": "3KWPcCVHpL1X5KnYNRFePUIhXba",
      "application_id": "3KWPcAXW4vQOLny1X1Yt1ntiQ3F",
      "contract_signed": false,
      "country": "MZ",
      "created_at": "2026-10-10T22:13:47.877114Z",
      "id": "3KWPcBhFkyaKP4fBYxzVtSYt15c",
      "kind": "lsp",
      "kyb_status": "pending",
      "language": "pt",
      "legal_name": "Águas de Teste UX SA",
      "region": "mz",
      "registration_number": "UX-1791670427842",
      "state_entered_at": "2026-10-10T22:13:47.877114Z",
      "status": "applied",
      "updated_at": "2026-10-10T22:13:47.877114Z"
    },
    {
      "account_id": "3KWM6F2mypStePkHm3btPTMpk78",
      "application_id": "3KWM6HfuaGcmeYDNvDyxniH4e21",
      "contract_signed": false,
      "country": "MZ",
      "created_at": "2026-10-10T21:44:53.291305Z",
      "id": "3KWM6DmypQMkZuhc0w4xYsd0Up3",
      "kind": "lsp",
      "kyb_status": "pending",
      "language": "pt",
      "legal_name": "Águas de Teste UX SA",
      "region": "mz",
      "registration_number": "UX-1791668693255",
      "state_entered_at": "2026-10-10T21:44:53.291305Z",
      "status": "applied",
      "updated_at": "2026-10-10T21:44:53.291305Z"
    }
  ]
}

GET /v1/admin/psps/{id}

A PSP and its lifecycle

The PSP, every transition with actor and reason, seconds spent in each state, the transitions allowed now, the pilot gates and the 1-day clock (seconds since KYB approval, to live). Requires accounts:read.

Parameters

NameInTypeDescription
id requiredpathstring

Responses

Request

curl -X GET "$KULPAY_URL/v1/admin/psps/{id}" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "allowed_transitions": [
    "offboarding",
    "suspended"
  ],
  "pilot_gates": [
    {
      "met": true,
      "name": "kyb_approved"
    },
    {
      "met": true,
      "name": "contract_signed"
    }
  ],
  "psp": {
    "account_id": "3KWPpsYfFNrQEhm84v3anWxHrHj",
    "application_id": "3KWPprbA3vQfJCZgfhOvbGOxKhO",
    "certified_at": "2026-10-10T22:15:36.187945473Z",
    "contract_signed": true,
    "country": "MZ",
    "created_at": "2026-10-10T22:15:36.187945473Z",
    "id": "3KWPpp00pI0ILGNVqqM3kUpSxNv",
    "kind": "psp",
    "kyb_status": "approved",
    "language": "pt",
    "legal_name": "Banco Exemplo SA",
    "live_at": "2026-10-18T22:15:36.187945473Z",
    "live_key_id": "3KWPpr5y02e3qlgVBBep5PQmDrF",
    "next_review_at": "2027-10-10T22:15:36.187945473Z",
    "pilot_started_at": "2026-10-10T22:15:36.187945473Z",
    "region": "mz",
    "registration_number": "100234567",
    "risk_tier": "medium",
    "sandbox_enabled_at": "2026-10-10T22:15:36.187945473Z",
    "sandbox_key_id": "3KWPps54CGNg8OL8aTqvdGqMbZb",
    "state_entered_at": "2026-10-18T22:15:36.187945473Z",
    "status": "live",
    "updated_at": "2026-10-18T22:15:36.187945473Z"
  },
  "seconds_in_state": {
    "applied": 0,
    "certification": 0,
    "kyb_review": 0,
    "live": 0,
    "pilot": 691200,
    "technical_setup": 0
  },
  "transitions": [
    {
      "actor": "operator:ana",
      "at": "2026-10-10T22:15:36.187945473Z",
      "from": "applied",
      "id": "3KWPpsw0BPdDgcswkkkys6ADGv2",
      "psp_id": "3KWPpp00pI0ILGNVqqM3kUpSxNv",
      "reason": "start",
      "seconds_in_previous_state": 0,
      "to": "kyb_review"
    },
    {
      "actor": "applicant",
      "at": "2026-10-10T22:15:36.187945473Z",
      "from": "",
      "id": "3KWPptE2FUaoKRJWuQ3s6AyWjt4",
      "psp_id": "3KWPpp00pI0ILGNVqqM3kUpSxNv",
      "reason": "application submitted",
      "seconds_in_previous_state": 0,
      "to": "applied"
    }
  ]
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "not_found",
  "error": {
    "code": "not_found",
    "message": "not_found"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

GET /v1/admin/psps/{id}/certification/runs

Certification runs

Parameters

NameInTypeDescription
id requiredpathstring

Responses

  • 200 Runs, newest first.

Request

curl -X GET "$KULPAY_URL/v1/admin/psps/{id}/certification/runs" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "data": [
    {
      "capability_matrix_version": 1,
      "cases": 13,
      "failed": 0,
      "id": "3KWPpMHNDSF9IxULJpbdY6NaH6F",
      "mandatory": 13,
      "passed": 0,
      "psp_id": "3KWPpNpKmnm7b4atBTqlciEy2Ky",
      "started_at": "2026-10-10T22:15:32.545774684Z",
      "started_by": "psp:3KWPpNpKmnm7b4atBTqlciEy2Ky",
      "status": "running"
    }
  ]
}

POST /v1/admin/psps/{id}/certification/runs

Start a certification run

Runs the suite generated from the approved capability matrix in the PSP's sandbox, in the background: for each payment method every outcome (success, each decline family, timeout, unknown resolved by status query, duplicate), refund, capture, void and payout flows; for a matrix that declares BILL_PAYMENT, a bill sold through the sandbox reference biller (REF_SANDBOX, from kulpay-payments sandbox seed) and one the biller refuses, refunded to the payer; for CASH_IN, a deposit initiated against a sandbox reference and confirmed, and one of the wrong amount refused; then idempotency replay, webhook delivery, redelivery and a badly signed webhook to the PSP's endpoint, and the sample statement, parsed with the parser and options of the PSP's statement profile (camt.053, camt.054, MT940, BAI2, CSV, …), matched against the run's payments. With rerun_of, only that run's failed cases run again; the passed ones are carried over. When every mandatory case passes, a report is signed (audit signing key) and a PSP in technical_setup moves to certification. Records psp.certification.run_started, case_failed, passed or failed. Requires accounts:write.

Parameters

NameInTypeDescription
id requiredpathstring

Request body application/json

FieldTypeDescription
rerun_ofstring

Responses

Request

curl -X POST "$KULPAY_URL/v1/admin/psps/{id}/certification/runs" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "rerun_of": "string"
}'
202 Response application/json
{
  "capability_matrix_version": 1,
  "cases": 13,
  "failed": 0,
  "id": "3KWPpcnth1lP0kp4qrkzjWdkBn9",
  "mandatory": 13,
  "passed": 0,
  "psp_id": "3KWPpNpKmnm7b4atBTqlciEy2Ky",
  "rerun_of": "3KWPpMHNDSF9IxULJpbdY6NaH6F",
  "started_at": "2026-10-10T22:15:34.655950849Z",
  "started_by": "key_accounts",
  "status": "running"
}
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "invalid_request: no approved capability matrix, sandbox not open.",
  "error": {
    "code": "invalid_request",
    "message": "invalid_request: no approved capability matrix, sandbox not open."
  },
  "status": 400,
  "title": "Bad Request",
  "type": "about:blank"
}
409 Error · certification_running application/problem+json
{
  "code": "certification_running",
  "detail": "A run is in progress (certification_running).",
  "error": {
    "code": "certification_running",
    "message": "A run is in progress (certification_running)."
  },
  "status": 409,
  "title": "Conflict",
  "type": "about:blank"
}

GET /v1/admin/psps/{id}/certification/runs/{run}

A run with its cases

Parameters

NameInTypeDescription
id requiredpathstring
run requiredpathstring

Responses

Request

curl -X GET "$KULPAY_URL/v1/admin/psps/{id}/certification/runs/{run}" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "cases": [
    {
      "actual": "status succeeded",
      "carried_from": "3KWPpMHNDSF9IxULJpbdY6NaH6F",
      "expected": "status succeeded",
      "id": "3KWPpb41DZtVM0Ha86ZeN0kgNg5",
      "key": "payment/card/success",
      "kind": "outcome",
      "mandatory": true,
      "payment_id": "3KWPpOxjsoZcAvZYdKnt3fVUcGX",
      "run_id": "3KWPpcnth1lP0kp4qrkzjWdkBn9",
      "seq": 1,
      "status": "passed",
      "title": "card payment succeeds"
    },
    {
      "actual": "status failed, failure_code card_declined",
      "carried_from": "3KWPpMHNDSF9IxULJpbdY6NaH6F",
      "expected": "status failed, failure_code card_declined",
      "id": "3KWPpaUUNF9C0XaZCAGg8Y2gsFv",
      "key": "payment/card/decline/card_error",
      "kind": "outcome",
      "mandatory": true,
      "payment_id": "3KWPpLpbGFUi7C15n4YcBkGn1AG",
      "run_id": "3KWPpcnth1lP0kp4qrkzjWdkBn9",
      "seq": 2,
      "status": "passed",
      "title": "card decline: card_error"
    }
  ],
  "run": {
    "capability_matrix_version": 1,
    "cases": 13,
    "failed": 0,
    "finished_at": "2026-10-10T22:15:34.719113129Z",
    "id": "3KWPpcnth1lP0kp4qrkzjWdkBn9",
    "mandatory": 13,
    "passed": 13,
    "psp_id": "3KWPpNpKmnm7b4atBTqlciEy2Ky",
    "report_sha256": "4ffffee44c214941549f25423c6505b12c9e18e4a51bde81a6d56f0a7b88c526",
    "rerun_of": "3KWPpMHNDSF9IxULJpbdY6NaH6F",
    "signature": "UCOtNjozn1QyAPH7R7nR0IQWN+phEJF1930iZtdDq71lmyZtw8Ypik3W27T5t27SkJQ1SGDG2alsO5bsyVMXAQ==",
    "signing_key_id": "cert-http",
    "started_at": "2026-10-10T22:15:34.655950849Z",
    "started_by": "key_accounts",
    "status": "passed"
  }
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "not_found",
  "error": {
    "code": "not_found",
    "message": "not_found"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

GET /v1/admin/psps/{id}/certification/runs/{run}/events

Follow a run live

Server-sent events: case ({key, status, actual, payment_id}) whenever a case changes, then run ({status, passed, failed}) when it finishes.

Parameters

NameInTypeDescription
id requiredpathstring
run requiredpathstring

Responses

  • 200 An event stream.

Request

curl -X GET "$KULPAY_URL/v1/admin/psps/{id}/certification/runs/{run}/events" \
  -H "Authorization: Bearer $KULPAY_KEY"

GET /v1/admin/psps/{id}/certification/runs/{run}/report.json

Signed report (JSON)

{report, sha256, signature, signing_key_id, public_key, algorithm}: verify the Ed25519 signature over the exact bytes of report.

Parameters

NameInTypeDescription
id requiredpathstring
run requiredpathstring

Responses

  • 200 The report.
  • 404 not_found (not finished).

Request

curl -X GET "$KULPAY_URL/v1/admin/psps/{id}/certification/runs/{run}/report.json" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "algorithm": "Ed25519 over the report bytes",
  "public_key": "/RckOFqgx1tk+3jNYC+h2ZH96/drE8WO1wLqyDXp9hg=",
  "report": {
    "capability_matrix_version": 1,
    "cases": [
      {
        "actual": "status succeeded",
        "carried_from": "3KWPpMHNDSF9IxULJpbdY6NaH6F",
        "expected": "status succeeded",
        "key": "payment/card/success",
        "mandatory": true,
        "payment_id": "3KWPpOxjsoZcAvZYdKnt3fVUcGX",
        "status": "passed",
        "title": "card payment succeeds"
      },
      {
        "actual": "status failed, failure_code card_declined",
        "carried_from": "3KWPpMHNDSF9IxULJpbdY6NaH6F",
        "expected": "status failed, failure_code card_declined",
        "key": "payment/card/decline/card_error",
        "mandatory": true,
        "payment_id": "3KWPpLpbGFUi7C15n4YcBkGn1AG",
        "status": "passed",
        "title": "card decline: card_error"
      }
    ],
    "failed": 0,
    "format": "kulpay.psp_certification.v1",
    "mandatory_cases": 13,
    "passed": 13,
    "psp": {
      "account_id": "3KWPpP8Jvi1WplT1iduLQKZYpeF",
      "id": "3KWPpNpKmnm7b4atBTqlciEy2Ky",
      "legal_name": "Banco Exemplo SA",
      "region": "mz"
    },
    "result": "passed",
    "run": {
      "duration_seconds": 0,
      "finished_at": "2026-10-10T22:15:34Z",
      "id": "3KWPpcnth1lP0kp4qrkzjWdkBn9",
      "rerun_of": "3KWPpMHNDSF9IxULJpbdY6NaH6F",
      "started_at": "2026-10-10T22:15:34Z"
    }
  },
  "sha256": "4ffffee44c214941549f25423c6505b12c9e18e4a51bde81a6d56f0a7b88c526",
  "signature": "UCOtNjozn1QyAPH7R7nR0IQWN+phEJF1930iZtdDq71lmyZtw8Ypik3W27T5t27SkJQ1SGDG2alsO5bsyVMXAQ==",
  "signing_key_id": "cert-http"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "not_found (not finished).",
  "error": {
    "code": "not_found",
    "message": "not_found (not finished)."
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

GET /v1/admin/psps/{id}/certification/runs/{run}/report.pdf

Report (PDF)

Parameters

NameInTypeDescription
id requiredpathstring
run requiredpathstring

Responses

  • 200 The report, with its SHA-256 and signature.

Request

curl -X GET "$KULPAY_URL/v1/admin/psps/{id}/certification/runs/{run}/report.pdf" \
  -H "Authorization: Bearer $KULPAY_KEY"

GET /v1/admin/psps/{id}/commissions

A PSP's commissions as a participant

What KulPay owes the PSP for its part in others' payments in a month (IO-4.4): each succeeded bill payment it took part in (initiating_psp, from_psp, to_psp) whose role a rule of its fee schedule prices (rule role), the amount posted to its balance with the payment as the reference, and the totals per currency. month is YYYY-MM (this month by default). Requires accounts:read.

Parameters

NameInTypeDescription
id requiredpathstring
monthquerystring
modequerystring
One of: live, test

Responses

  • 200 The statement.

Request

curl -X GET "$KULPAY_URL/v1/admin/psps/{id}/commissions" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "commissions": [
    {
      "account_id": "3KXgACCkulOffice0000000001",
      "amount_minor": 1250,
      "base_minor": 125000,
      "created_at": "2026-10-15T10:00:00Z",
      "currency": "MZN",
      "id": "3KXgCOM00000000000000000001",
      "mode": "live",
      "payment_id": "3KXgBILL0000000000000000001",
      "payment_kind": "bill_payment",
      "psp_id": "3KXgPSPkulOffice00000000001",
      "role": "initiating_psp",
      "rule": 0,
      "schedule_id": "3KXgFEE00000000000000000001",
      "schedule_version": 2
    }
  ],
  "mode": "live",
  "month": "2026-10",
  "psp_id": "3KXgPSPkulOffice00000000001",
  "totals": {
    "MZN": "12.50"
  }
}

GET /v1/admin/psps/{id}/configs

Configuration versions

Every version of every configuration kind (or one kind), newest first. Requires accounts:read.

Parameters

NameInTypeDescription
id requiredpathstring
kindquerystring
One of: capability_matrix, orientation_profile, routing, retry_policy, commercial_terms, statement_profile, technical_config

Responses

Request

curl -X GET "$KULPAY_URL/v1/admin/psps/{id}/configs" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "data": [
    {
      "approval_id": "3KWPplZqDOrQjHFV064vY3MD58T",
      "based_on": 1,
      "created_at": "2026-10-10T22:15:35.427494098Z",
      "created_by": "key_accounts",
      "document": {
        "contract_ref": "KP-2026-014",
        "fee_schedule": [
          {
            "currency": "MZN",
            "fixed": "0",
            "method": "mpesa",
            "percent": "1.5"
          }
        ],
        "funds_flow": "collecting",
        "limits": {
          "currency": "MZN",
          "daily": "1000000",
          "monthly": "20000000",
          "per_transaction": "400000"
        },
        "settlement": {
          "account": "BCI 0001",
          "currency": "MZN",
          "frequency_days": 1
        }
      },
      "id": "3KWPpkZNsNRnKt4lDIVa7a0ns7t",
      "kind": "commercial_terms",
      "psp_id": "3KWPpmM4QtZQAtFMAzZOdpsmCab",
      "reason": "limit too low",
      "status": "pending",
      "version": 3
    },
    {
      "activated_at": "2026-10-10T22:15:35.350756793Z",
      "approval_id": "3KWPpjjLsFZT3cq9khF4WJcFINP",
      "created_at": "2026-10-10T22:15:35.322544516Z",
      "created_by": "key_accounts",
      "document": {
        "contract_ref": "KP-2026-014",
        "fee_schedule": [
          {
            "currency": "MZN",
            "fixed": "0",
            "method": "mpesa",
            "percent": "1.5"
          }
        ],
        "funds_flow": "collecting",
        "limits": {
          "currency": "MZN",
          "daily": "1000000",
          "monthly": "20000000",
          "per_transaction": "300000"
        },
        "settlement": {
          "account": "BCI 0001",
          "currency": "MZN",
          "frequency_days": 1
        }
      },
      "id": "3KWPphHmJysI9m55s7C6dpxyJ7p",
      "kind": "commercial_terms",
      "psp_id": "3KWPpmM4QtZQAtFMAzZOdpsmCab",
      "reason": "setup",
      "status": "active",
      "version": 2
    }
  ],
  "kinds": [
    "capability_matrix",
    "orientation_profile"
  ]
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "not_found",
  "error": {
    "code": "not_found",
    "message": "not_found"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

POST /v1/admin/psps/{id}/configs/{kind}

Create a configuration version

Creates the next version and records config.<kind>.version_created (and psp.commercial_terms.version_created). commercial_terms, retry_policy (a change to a money-moving pattern) and capability_matrix need a second person: the version stays pending with a config.activate approval request (202) until approved, then becomes active and supersedes the previous one (config.<kind>.activated). Other kinds are active at once (201). Only one version of a kind may be pending. Requires accounts:write. The routing kind is retired: 410 config_kind_retired. technical_config has the sections endpoints, credentials, webhooks and ip_allowlist, each checked as the onboarding portal checks it (https URLs and no private addresses, PEM certificates and PGP keys that parse, public CIDRs no broader than /24, catalogue event types); a failure is a 400 naming section, check and detail. Endpoint reachability over TLS is probed in the background and recorded on the setup step (psp.technical_step.probed).

Parameters

NameInTypeDescription
id requiredpathstring
kind requiredpathstring
One of: capability_matrix, orientation_profile, routing, retry_policy, commercial_terms, statement_profile, technical_config

Request body application/json required

FieldTypeDescription
document requiredobject
reason requiredstring

Responses

  • 201 Active at once.
  • 202 Waiting for approval.
  • 400 Invalid document (invalid_request), e.g. commercial terms above the risk tier, or a technical_config section that fails a check: technical config: endpoints: callback URL: must use https.
  • 404 not_found
  • 410 The kind is retired (config_kind_retired): routing, which the router never read, is configured in the global routing rules. The body names kind and successor; the Link header (rel="successor-version") points to it. Versions of a retired kind stay readable.

Request

curl -X POST "$KULPAY_URL/v1/admin/psps/{id}/configs/{kind}" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "document": {
    "contract_ref": "KP-2026-014",
    "fee_schedule": [
      {
        "currency": "MZN",
        "fixed": "0",
        "method": "mpesa",
        "percent": "1.5"
      }
    ],
    "funds_flow": "collecting",
    "limits": {
      "currency": "MZN",
      "daily": "1000000",
      "monthly": "20000000",
      "per_transaction": "400000"
    },
    "settlement": {
      "account": "BCI 0001",
      "currency": "MZN",
      "frequency_days": 1
    }
  },
  "reason": "setup"
}'
201 Response application/json
{
  "version": {
    "activated_at": "2026-10-10T22:15:35.38296785Z",
    "created_at": "2026-10-10T22:15:35.380789122Z",
    "created_by": "key_accounts",
    "id": "3KWPpkrJxAYv4GusdsaY9P0DQj5",
    "kind": "technical_config",
    "psp_id": "3KWPpmM4QtZQAtFMAzZOdpsmCab",
    "reason": "setup",
    "status": "active",
    "version": 1
  }
}
202 Response application/json
{
  "approval": {
    "created_at": "2026-10-10T22:15:35.241837569Z",
    "id": "3KWPphBZ9VTn7DG5zPpow7M3U5Q",
    "kind": "config.activate",
    "payload": "{\"version_id\":\"3KWPpmre6hzgOpcO2dDy9gATCiE\",\"kind\":\"commercial_terms\",\"version\":1}",
    "prepared_by": "key_accounts",
    "reason": "commercial_terms v1: setup",
    "status": "pending",
    "subject_id": "3KWPpmM4QtZQAtFMAzZOdpsmCab",
    "tenant_id": "3KWPpklQ3UMKtjqz5XSeoWPYWKh"
  },
  "message": "version prepared; a second person must approve it before it is active",
  "version": {
    "approval_id": "3KWPphBZ9VTn7DG5zPpow7M3U5Q",
    "created_at": "2026-10-10T22:15:35.240082387Z",
    "created_by": "key_accounts",
    "id": "3KWPpmre6hzgOpcO2dDy9gATCiE",
    "kind": "commercial_terms",
    "psp_id": "3KWPpmM4QtZQAtFMAzZOdpsmCab",
    "reason": "setup",
    "status": "pending",
    "version": 1
  }
}
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "commercial terms: set the risk tier before commercial terms: limits may not exceed its defaults",
  "error": {
    "code": "invalid_request",
    "message": "commercial terms: set the risk tier before commercial terms: limits may not exceed its defaults"
  },
  "status": 400,
  "title": "Bad Request",
  "type": "about:blank"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "not_found",
  "error": {
    "code": "not_found",
    "message": "not_found"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}
410 Error · config_kind_retired application/problem+json
{
  "code": "config_kind_retired",
  "detail": "The routing configuration kind is retired; its versions stay readable. Configure it at /v1/routing/versions.",
  "error": {
    "code": "config_kind_retired",
    "message": "The routing configuration kind is retired; its versions stay readable. Configure it at /v1/routing/versions."
  },
  "kind": "routing",
  "status": 410,
  "successor": "/v1/routing/versions",
  "title": "Configuration kind retired",
  "type": "about:blank"
}

GET /v1/admin/psps/{id}/configs/{kind}/diff

Compare two versions

Field-by-field differences by JSON path (e.g. limits.daily, fee_schedule[0].percent).

Parameters

NameInTypeDescription
id requiredpathstring
kind requiredpathstring
One of: capability_matrix, orientation_profile, routing, retry_policy, commercial_terms, statement_profile, technical_config
from requiredqueryinteger
to requiredqueryinteger

Responses

Request

curl -X GET "$KULPAY_URL/v1/admin/psps/{id}/configs/{kind}/diff" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "changes": [
    {
      "change": "changed",
      "from": "400000",
      "path": "limits.per_transaction",
      "to": "300000"
    }
  ],
  "from": 1,
  "to": 2
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "not_found",
  "error": {
    "code": "not_found",
    "message": "not_found"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

GET /v1/admin/psps/{id}/configs/{kind}/{version}

One configuration version

Parameters

NameInTypeDescription
id requiredpathstring
kind requiredpathstring
One of: capability_matrix, orientation_profile, routing, retry_policy, commercial_terms, statement_profile, technical_config
version requiredpathinteger

Responses

  • 200 The version with its document.
  • 404 not_found

Request

curl -X GET "$KULPAY_URL/v1/admin/psps/{id}/configs/{kind}/{version}" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "activated_at": "2026-10-10T22:15:35.281788234Z",
  "approval_id": "3KWPphBZ9VTn7DG5zPpow7M3U5Q",
  "created_at": "2026-10-10T22:15:35.240082387Z",
  "created_by": "key_accounts",
  "document": {
    "contract_ref": "KP-2026-014",
    "fee_schedule": [
      {
        "currency": "MZN",
        "fixed": "0",
        "method": "mpesa",
        "percent": "1.5"
      }
    ],
    "funds_flow": "collecting",
    "limits": {
      "currency": "MZN",
      "daily": "1000000",
      "monthly": "20000000",
      "per_transaction": "400000"
    },
    "settlement": {
      "account": "BCI 0001",
      "currency": "MZN",
      "frequency_days": 1
    }
  },
  "id": "3KWPpmre6hzgOpcO2dDy9gATCiE",
  "kind": "commercial_terms",
  "psp_id": "3KWPpmM4QtZQAtFMAzZOdpsmCab",
  "reason": "setup",
  "status": "superseded",
  "version": 1
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "configuration version not found",
  "error": {
    "code": "not_found",
    "message": "configuration version not found"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

POST /v1/admin/psps/{id}/configs/{kind}/{version}/rollback

Roll back to a version

Creates a new version copying the given one (based_on), through the same approval path as any change.

Parameters

NameInTypeDescription
id requiredpathstring
kind requiredpathstring
One of: capability_matrix, orientation_profile, routing, retry_policy, commercial_terms, statement_profile, technical_config
version requiredpathinteger

Request body application/json required

FieldTypeDescription
reason requiredstring

Responses

  • 201 Active at once.
  • 202 Waiting for approval.
  • 400 invalid_request
  • 404 not_found
  • 410 The kind is retired (config_kind_retired): routing, which the router never read, is configured in the global routing rules. The body names kind and successor; the Link header (rel="successor-version") points to it. Versions of a retired kind stay readable.

Request

curl -X POST "$KULPAY_URL/v1/admin/psps/{id}/configs/{kind}/{version}/rollback" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "reason": "limit too low"
}'
201 Response application/json
{
  "approval": {
    "created_at": "2026-10-10T09:30:00Z",
    "decided_at": "2026-10-10T09:30:00Z",
    "decided_by": "string",
    "decision_reason": "Pagamento de energia, outubro",
    "id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
    "kind": "standard",
    "prepared_by": "string",
    "reason": "Pagamento de energia, outubro",
    "status": "pending",
    "subject_id": "3KUXQ43bztiuf9tCMNgbcrYvne3"
  },
  "message": "Pagamento de energia, outubro",
  "version": {
    "activated_at": "2026-10-10T09:30:00Z",
    "approval_id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
    "based_on": 1,
    "created_at": "2026-10-10T09:30:00Z",
    "created_by": "string",
    "document": {},
    "id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
    "kind": "capability_matrix",
    "psp_id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
    "reason": "Pagamento de energia, outubro",
    "status": "pending",
    "version": 1
  }
}
202 Response application/json
{
  "approval": {
    "created_at": "2026-10-10T22:15:35.429536568Z",
    "id": "3KWPplZqDOrQjHFV064vY3MD58T",
    "kind": "config.activate",
    "payload": "{\"version_id\":\"3KWPpkZNsNRnKt4lDIVa7a0ns7t\",\"kind\":\"commercial_terms\",\"version\":3}",
    "prepared_by": "key_accounts",
    "reason": "commercial_terms v3: limit too low",
    "status": "pending",
    "subject_id": "3KWPpmM4QtZQAtFMAzZOdpsmCab",
    "tenant_id": "3KWPpklQ3UMKtjqz5XSeoWPYWKh"
  },
  "message": "version prepared; a second person must approve it before it is active",
  "version": {
    "approval_id": "3KWPplZqDOrQjHFV064vY3MD58T",
    "based_on": 1,
    "created_at": "2026-10-10T22:15:35.427494098Z",
    "created_by": "key_accounts",
    "id": "3KWPpkZNsNRnKt4lDIVa7a0ns7t",
    "kind": "commercial_terms",
    "psp_id": "3KWPpmM4QtZQAtFMAzZOdpsmCab",
    "reason": "limit too low",
    "status": "pending",
    "version": 3
  }
}
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "invalid_request",
  "error": {
    "code": "invalid_request",
    "message": "invalid_request"
  },
  "status": 400,
  "title": "Bad Request",
  "type": "about:blank"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "not_found",
  "error": {
    "code": "not_found",
    "message": "not_found"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}
410 Error · config_kind_retired application/problem+json
{
  "code": "config_kind_retired",
  "detail": "The routing configuration kind is retired; its versions stay readable. Configure it at /v1/routing/versions.",
  "error": {
    "code": "config_kind_retired",
    "message": "The routing configuration kind is retired; its versions stay readable. Configure it at /v1/routing/versions."
  },
  "kind": "routing",
  "status": 410,
  "successor": "/v1/routing/versions",
  "title": "Configuration kind retired",
  "type": "about:blank"
}

POST /v1/admin/psps/{id}/contract

Record the signed contract

Records the signed contract for the active commercial terms (contract_ref comes from them) and emits psp.contract.signed. It satisfies the contract_signed pilot gate. Requires approved commercial terms.

Parameters

NameInTypeDescription
id requiredpathstring

Request body multipart/form-data required

FieldTypeDescription
file requiredstring (binary) The signed copy, up to 10 MB; stored encrypted with its SHA-256.
reason requiredstring

Responses

Request

curl -X POST "$KULPAY_URL/v1/admin/psps/{id}/contract" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -F "file=@./document.pdf"
200 Response application/json
{
  "account_id": "3KWPpklQ3UMKtjqz5XSeoWPYWKh",
  "application_id": "3KWPpo6ixiv7LczATPXP4CQXpS6",
  "contract_ref": "KP-2026-014",
  "contract_signed": true,
  "country": "MZ",
  "created_at": "2026-10-10T22:15:35.185876129Z",
  "id": "3KWPpmM4QtZQAtFMAzZOdpsmCab",
  "kind": "psp",
  "kyb_status": "pending",
  "language": "pt",
  "legal_name": "Banco Exemplo SA",
  "next_review_at": "2027-10-10T22:15:35.218295061Z",
  "region": "",
  "registration_number": "100234567",
  "risk_tier": "medium",
  "state_entered_at": "2026-10-10T22:15:35.185876129Z",
  "status": "applied",
  "updated_at": "2026-10-10T22:15:35.528536855Z"
}
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "invalid_request",
  "error": {
    "code": "invalid_request",
    "message": "invalid_request"
  },
  "status": 400,
  "title": "Bad Request",
  "type": "about:blank"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "not_found",
  "error": {
    "code": "not_found",
    "message": "not_found"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

GET /v1/admin/psps/{id}/credit-report

A resolver's credits of a day

A resolver's credits applied on a day (IO-6.2), yesterday by default: deposit, credit, amount, status, when applied, the resolver's transaction and the settlement batch that paid it, with the totals per currency. ?format=csv answers CSV. Requires reconciliation:read.

Parameters

NameInTypeDescription
id requiredpathstring
datequerystring
modequerystring
formatquerystring

Responses

  • 200 The report.

Request

curl -X GET "$KULPAY_URL/v1/admin/psps/{id}/credit-report" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "credits": [
    {
      "amount": "500.00",
      "applied_at": "2026-10-14T09:12:00Z",
      "credit_id": "3KXgCRD00000000000000000001",
      "currency": "MZN",
      "deposit_id": "3KXgDEP00000000000000000001",
      "settlement_batch_id": "3KXgBAT00000000000000000001",
      "status": "applied",
      "transaction_id": "txn_blnk_1"
    }
  ],
  "date": "2026-10-14",
  "mode": "live",
  "resolver_psp_id": "3KXgPSPkulOffice00000000001",
  "totals": {
    "MZN": "500.00"
  }
}

GET /v1/admin/psps/{id}/go-live

Pilot and go-live state

Pilot gates (KYB approved, contract signed, commercial terms approved, certification passed), live gates (7 days in pilot since entry or the last critical incident resolved, no open critical incident, for a PSP, live statements since pilot all matched; for a biller, a live remittance paid during the pilot, or clean supplier reconciliations when it sells only principal-model products), pilot settings, live key and incidents.

Parameters

NameInTypeDescription
id requiredpathstring

Responses

  • 200 The state.

Request

curl -X GET "$KULPAY_URL/v1/admin/psps/{id}/go-live" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "go_live_checklist_stored": false,
  "incidents": [],
  "live_gates": [
    {
      "detail": "0 of 7 days since 2026-10-10 22:15",
      "met": false,
      "name": "pilot_7_days"
    },
    {
      "detail": "0 open critical incident(s)",
      "met": true,
      "name": "no_critical_incident"
    }
  ],
  "live_key_id": "3KWPpr5y02e3qlgVBBep5PQmDrF",
  "pilot_gates": [
    {
      "met": true,
      "name": "kyb_approved"
    },
    {
      "met": true,
      "name": "contract_signed"
    }
  ],
  "pilot_settings": {
    "currency": "MZN",
    "daily": "100000",
    "merchants": [],
    "monthly": "2000000",
    "per_transaction": "40000",
    "routing_weight": 10
  },
  "status": "pilot"
}

POST /v1/admin/psps/{id}/go-live

Prepare go-live (maker)

From pilot, with every live gate met, prepares a psp.go_live approval that a different person decides (POST /v1/admin/approvals/{id}/decide). Approval stores the go-live checklist, with the signed certification report, and moves the PSP to live. Moving to live any other way is refused.

Parameters

NameInTypeDescription
id requiredpathstring

Request body application/json required

FieldTypeDescription
reason requiredstring

Responses

Request

curl -X POST "$KULPAY_URL/v1/admin/psps/{id}/go-live" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "reason": "7 clean days"
}'
202 Response application/json
{
  "approval": {
    "created_at": "2026-10-18T22:15:36.187945473Z",
    "id": "3KWPpq3PJfN5Rmqed0WaMbdtOKc",
    "kind": "psp.go_live",
    "prepared_by": "key_accounts",
    "reason": "7 clean days",
    "status": "pending",
    "subject_id": "3KWPpp00pI0ILGNVqqM3kUpSxNv",
    "tenant_id": "3KWPpsYfFNrQEhm84v3anWxHrHj"
  },
  "message": "go-live prepared; a second person must approve it (POST /v1/admin/approvals/{id}/decide)"
}
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "live gates not met: pilot_7_days, no_critical_incident, reconciliation_matched",
  "error": {
    "code": "invalid_request",
    "message": "live gates not met: pilot_7_days, no_critical_incident, reconciliation_matched"
  },
  "status": 400,
  "title": "Bad Request",
  "type": "about:blank"
}

GET /v1/admin/psps/{id}/go-live/checklist

Go-live checklist

What was true when go-live was approved: approvers, pilot and live gates, KYB, contract, commercial terms version, the signed certification report, pilot settings and incidents.

Parameters

NameInTypeDescription
id requiredpathstring

Responses

  • 200 The checklist.
  • 404 not_found: not live through an approval.

Request

curl -X GET "$KULPAY_URL/v1/admin/psps/{id}/go-live/checklist" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "approval": {
    "approved_by": "key_checker",
    "id": "3KWPpq3PJfN5Rmqed0WaMbdtOKc",
    "prepared_by": "key_accounts",
    "reason": "7 clean days"
  },
  "certification": {
    "run_id": ""
  },
  "commercial_terms_version": 1,
  "contract": {
    "ref": "",
    "signed": true
  },
  "format": "kulpay.psp_go_live_checklist.v1",
  "generated_at": "2026-10-18T22:15:36Z",
  "incidents": [
    {
      "id": "3KWPpuIL9mUgnP4edWkQbxtFzg9",
      "opened_at": "2026-10-10T22:15:36.187945473Z",
      "opened_by": "key_accounts",
      "psp_id": "3KWPpp00pI0ILGNVqqM3kUpSxNv",
      "resolution": "fixed",
      "resolved_at": "2026-10-10T22:15:36.187945473Z",
      "resolved_by": "key_accounts",
      "severity": "critical",
      "summary": "callbacks stalled"
    }
  ],
  "kyb": {
    "approved_at": null,
    "risk_tier": "medium",
    "status": "approved"
  },
  "live_gates": [
    {
      "detail": "8 of 7 days since 2026-10-10 22:15",
      "met": true,
      "name": "pilot_7_days"
    },
    {
      "detail": "0 open critical incident(s)",
      "met": true,
      "name": "no_critical_incident"
    }
  ],
  "pilot": {
    "entered_at": "2026-10-10T22:15:36.187945473Z",
    "settings": {
      "currency": "MZN",
      "daily": "100000",
      "merchants": [
        "m-1"
      ],
      "monthly": "2000000",
      "per_transaction": "40000",
      "routing_weight": 20
    },
    "started_at": "2026-10-10T22:15:36.187945473Z"
  },
  "pilot_gates": [
    {
      "met": true,
      "name": "kyb_approved"
    },
    {
      "met": true,
      "name": "contract_signed"
    }
  ],
  "psp": {
    "account_id": "3KWPpsYfFNrQEhm84v3anWxHrHj",
    "id": "3KWPpp00pI0ILGNVqqM3kUpSxNv",
    "legal_name": "Banco Exemplo SA",
    "region": "mz"
  }
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "not_found: not live through an approval.",
  "error": {
    "code": "not_found",
    "message": "not_found: not live through an approval."
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

POST /v1/admin/psps/{id}/incidents

Record an incident

A critical incident holds go-live; the 7 pilot days count again from its resolution.

Parameters

NameInTypeDescription
id requiredpathstring

Request body application/json required

FieldTypeDescription
severity requiredstring
One of: critical, major, minor
summary requiredstring

Responses

  • 201 The incident.

Request

curl -X POST "$KULPAY_URL/v1/admin/psps/{id}/incidents" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "severity": "critical",
  "summary": "callbacks stalled"
}'
201 Response application/json
{
  "id": "3KWPpuIL9mUgnP4edWkQbxtFzg9",
  "opened_at": "2026-10-10T22:15:36.187945473Z",
  "opened_by": "key_accounts",
  "psp_id": "3KWPpp00pI0ILGNVqqM3kUpSxNv",
  "severity": "critical",
  "summary": "callbacks stalled"
}

POST /v1/admin/psps/{id}/incidents/{incident}/resolve

Resolve an incident

Parameters

NameInTypeDescription
id requiredpathstring
incident requiredpathstring

Request body application/json required

FieldTypeDescription
resolution requiredstring

Responses

  • 200 The incident.

Request

curl -X POST "$KULPAY_URL/v1/admin/psps/{id}/incidents/{incident}/resolve" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "resolution": "fixed"
}'
200 Response application/json
{
  "id": "3KWPpuIL9mUgnP4edWkQbxtFzg9",
  "opened_at": "2026-10-10T22:15:36.187945473Z",
  "opened_by": "key_accounts",
  "psp_id": "3KWPpp00pI0ILGNVqqM3kUpSxNv",
  "resolution": "fixed",
  "resolved_at": "2026-10-10T22:15:36.187945473Z",
  "resolved_by": "key_accounts",
  "severity": "critical",
  "summary": "callbacks stalled"
}

PUT /v1/admin/psps/{id}/institution

Set a PSP's bank code and wallet prefixes

The PSP's place in the payment system (IO-4.2): the 4 digits its NIBs start with and its mobile wallets' MSISDN prefixes (international digits). KulPay infers by them the institution of a biller's credit account (to_psp) and of a deposit's bank (from_psp); no other PSP may hold the same code or an overlapping prefix. Records psp.institution_updated. Requires accounts:write.

Parameters

NameInTypeDescription
id requiredpathstring

Request body application/json required

FieldTypeDescription
bank_codestring
reasonstring
wallet_methodstring The payout method its wallets are paid by: remittances to a biller's wallet go through it (IO-4.3).
wallet_prefixesstring[]

Responses

Request

curl -X PUT "$KULPAY_URL/v1/admin/psps/{id}/institution" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "bank_code": "0008",
  "reason": "BCI",
  "wallet_prefixes": []
}'
200 Response application/json
{
  "account_id": "3KWPnlM19nPUwG6nNuynNezp1w7",
  "application_id": "3KWPnhekPhfRi1KsQ8oQk3l33ma",
  "contract_signed": false,
  "country": "MZ",
  "created_at": "2026-10-10T22:15:19.816714735Z",
  "id": "3KWPnnBS0h6YDMR6gHeiAUNjVFo",
  "kind": "psp",
  "kyb_status": "in_review",
  "language": "pt",
  "legal_name": "Banco Exemplo SA",
  "region": "",
  "registration_number": "100234567",
  "sandbox_enabled_at": "2026-10-10T22:15:19.916518232Z",
  "sandbox_key_id": "3KWPnhHlWvZRFKddEUPjtBP6SFc",
  "state_entered_at": "2026-10-10T22:15:19.8755279Z",
  "status": "technical_setup",
  "updated_at": "2026-10-10T22:15:19.947987543Z"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "not_found",
  "error": {
    "code": "not_found",
    "message": "not_found"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}
409 Error · sandbox_unavailable application/problem+json
{
  "code": "sandbox_unavailable",
  "detail": "Not in technical_setup or later (sandbox_unavailable).",
  "error": {
    "code": "sandbox_unavailable",
    "message": "Not in technical_setup or later (sandbox_unavailable)."
  },
  "status": 409,
  "title": "Conflict",
  "type": "about:blank"
}

GET /v1/admin/psps/{id}/kyb

KYB review state

Checklist (documents by type, sanctions and PEP screening, risk tier, blockers, pending approvals), documents, screenings and the risk-tier defaults. Requires accounts:read.

Parameters

NameInTypeDescription
id requiredpathstring

Responses

  • 200 The review.

Request

curl -X GET "$KULPAY_URL/v1/admin/psps/{id}/kyb" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "checklist": {
    "blockers": [
      "document licence: missing",
      "document registration_certificate: missing"
    ],
    "documents": {
      "aml_policy": "missing",
      "articles": "missing",
      "director_ids": "missing",
      "financial_statements": "missing",
      "licence": "missing",
      "registration_certificate": "missing",
      "ubo_declaration": "missing"
    },
    "pending_approvals": [],
    "ready_for_approval": false,
    "risk_tier": "",
    "screenings": {
      "pep": "missing",
      "sanctions": "missing"
    }
  },
  "documents": [],
  "risk_tiers": {
    "high": {
      "currency": "MZN",
      "daily_limit": "5000000",
      "monthly_limit": "100000000",
      "per_transaction_limit": "100000",
      "review_months": 6
    },
    "low": {
      "currency": "MZN",
      "daily_limit": "50000000",
      "monthly_limit": "1000000000",
      "per_transaction_limit": "1000000",
      "review_months": 24
    },
    "medium": {
      "currency": "MZN",
      "daily_limit": "20000000",
      "monthly_limit": "400000000",
      "per_transaction_limit": "500000",
      "review_months": 12
    }
  },
  "screenings": []
}

POST /v1/admin/psps/{id}/kyb/decision

Decide KYB

approve prepares a maker-checker request (202) that a different officer decides; reject rejects KYB and the PSP; request_info emails the PSP what is missing and moves it to info_requested.

Parameters

NameInTypeDescription
id requiredpathstring

Request body application/json required

FieldTypeDescription
decision requiredstring
One of: approve, reject, request_info
missingstring[]
reason requiredstring

Responses

  • 200 The PSP.
  • 202 Approval prepared.

Request

curl -X POST "$KULPAY_URL/v1/admin/psps/{id}/kyb/decision" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "decision": "approve",
  "reason": "complete"
}'
200 Response application/json
{
  "account_id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
  "application_id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
  "certification_run_id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
  "certified_at": "2026-10-10T09:30:00Z",
  "contract_ref": "string",
  "contract_signed": true,
  "country": "MZ",
  "emergency_suspended_at": "2026-10-10T09:30:00Z",
  "id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
  "kyb_approved_at": "2026-10-10T09:30:00Z",
  "kyb_status": "pending",
  "language": "pt",
  "last_review_at": "2026-10-10T09:30:00Z",
  "legal_name": "Electricidade de Moçambique",
  "live_at": "2026-10-10T09:30:00Z",
  "live_key_id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
  "pilot_started_at": "2026-10-10T09:30:00Z",
  "region": "string",
  "registration_number": "string",
  "resume_state": "succeeded",
  "review_notice": "",
  "risk_tier": "string",
  "sandbox_enabled_at": "2026-10-10T09:30:00Z",
  "sandbox_key_id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
  "state_entered_at": "2026-10-10T09:30:00Z",
  "status": "applied",
  "suspension_review_id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
  "suspension_review_overdue": true
}
202 Response application/json
{
  "approval": {
    "created_at": "2026-10-10T22:15:38.073385131Z",
    "id": "3KWPq4nYTn5iKz3zaFVWv7HBAsO",
    "kind": "psp.kyb.approve",
    "prepared_by": "key_accounts",
    "reason": "complete",
    "status": "pending",
    "subject_id": "3KWPpxo3jWfN94PGtqWEXcMLDgp",
    "tenant_id": "3KWPq2SN77xFNSG3RWceD51rpAl"
  },
  "message": "KYB approval prepared; a second compliance officer must approve it"
}

POST /v1/admin/psps/{id}/kyb/documents

Upload a KYB document (operator)

Parameters

NameInTypeDescription
id requiredpathstring

Request body multipart/form-data required

FieldTypeDescription
file requiredstring (binary) Up to 10 MB.
type requiredstring
One of: licence, registration_certificate, articles, ubo_declaration, director_ids, aml_policy, financial_statements

Responses

  • 201 The document.

Request

curl -X POST "$KULPAY_URL/v1/admin/psps/{id}/kyb/documents" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -F "file=@./document.pdf"
201 Response application/json
{
  "file_id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
  "filename": "Electricidade de Moçambique",
  "id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
  "psp_id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
  "review_comment": "string",
  "reviewed_at": "2026-10-10T09:30:00Z",
  "reviewed_by": "string",
  "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
  "status": "pending",
  "type": "standard",
  "uploaded_at": "2026-10-10T09:30:00Z"
}

GET /v1/admin/psps/{id}/kyb/documents/{doc}

Download a KYB document

Decrypted, after its content is checked against its checksum (X-Content-SHA256). Requires accounts:read.

Parameters

NameInTypeDescription
id requiredpathstring
doc requiredpathstring

Responses

  • 200 The document.
  • 409 The stored document does not match its checksum (document_tampered).

Request

curl -X GET "$KULPAY_URL/v1/admin/psps/{id}/kyb/documents/{doc}" \
  -H "Authorization: Bearer $KULPAY_KEY"
409 Error · document_tampered application/problem+json
{
  "code": "document_tampered",
  "detail": "The stored document does not match its checksum (document_tampered).",
  "error": {
    "code": "document_tampered",
    "message": "The stored document does not match its checksum (document_tampered)."
  },
  "status": 409,
  "title": "Conflict",
  "type": "about:blank"
}

POST /v1/admin/psps/{id}/kyb/documents/{doc}/review

Accept or reject a document

Parameters

NameInTypeDescription
id requiredpathstring
doc requiredpathstring

Request body application/json required

FieldTypeDescription
commentstring Required when rejecting.
decision requiredstring
One of: accepted, rejected

Responses

  • 200 The document.

Request

curl -X POST "$KULPAY_URL/v1/admin/psps/{id}/kyb/documents/{doc}/review" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "decision": "accepted"
}'
200 Response application/json
{
  "file_id": "3KWPq4JEwiZkjmlc4D4BQL6xx0j",
  "filename": "licence.pdf",
  "id": "3KWPq3VocBtK5YJsaPSzJXtujyD",
  "psp_id": "3KWPpxo3jWfN94PGtqWEXcMLDgp",
  "reviewed_at": "2026-10-10T22:15:37.820822651Z",
  "reviewed_by": "key_accounts",
  "sha256": "4cff4e3de93bfed4899f4cbfc7be8b77cdafec9560a995cd6092c00c8ba75302",
  "status": "accepted",
  "type": "licence",
  "uploaded_at": "2026-10-10T22:15:37.433724445Z"
}

POST /v1/admin/psps/{id}/kyb/risk-tier

Set the risk tier

Sets default limits and the review frequency (high 6 months, medium 12, low 24).

Parameters

NameInTypeDescription
id requiredpathstring

Request body application/json required

FieldTypeDescription
reason requiredstring
tier requiredstring
One of: low, medium, high

Responses

  • 200 The PSP.

Request

curl -X POST "$KULPAY_URL/v1/admin/psps/{id}/kyb/risk-tier" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "reason": "profile",
  "tier": "medium"
}'
200 Response application/json
{
  "account_id": "3KWPpklQ3UMKtjqz5XSeoWPYWKh",
  "application_id": "3KWPpo6ixiv7LczATPXP4CQXpS6",
  "contract_signed": false,
  "country": "MZ",
  "created_at": "2026-10-10T22:15:35.185876129Z",
  "id": "3KWPpmM4QtZQAtFMAzZOdpsmCab",
  "kind": "psp",
  "kyb_status": "pending",
  "language": "pt",
  "legal_name": "Banco Exemplo SA",
  "next_review_at": "2027-10-10T22:15:35.218295061Z",
  "region": "",
  "registration_number": "100234567",
  "risk_tier": "medium",
  "state_entered_at": "2026-10-10T22:15:35.185876129Z",
  "status": "applied",
  "updated_at": "2026-10-10T22:15:35.218295061Z"
}

POST /v1/admin/psps/{id}/kyb/screenings

Record a manual sanctions or PEP check

Stores the check with its evidence; emits psp.screening.completed and, for a hit, psp.screening.hit.

Parameters

NameInTypeDescription
id requiredpathstring

Request body application/json required

FieldTypeDescription
evidence_file_idstring
kind requiredstring
One of: sanctions, pep
notesstring
referencestring
result requiredstring
One of: clear, hit
subject requiredstring

Responses

  • 201 The screening.

Request

curl -X POST "$KULPAY_URL/v1/admin/psps/{id}/kyb/screenings" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "kind": "sanctions",
  "reference": "UN, EU, OFAC lists 2026-10-08",
  "result": "clear",
  "subject": "Banco Exemplo SA"
}'
201 Response application/json
{
  "at": "2026-10-10T22:15:37.992225153Z",
  "id": "3KWPq13Jp78LpXfEc3b4aNkTdSe",
  "kind": "sanctions",
  "performed_by": "key_accounts",
  "provider": "manual",
  "psp_id": "3KWPpxo3jWfN94PGtqWEXcMLDgp",
  "reference": "UN, EU, OFAC lists 2026-10-08",
  "result": "clear",
  "subject": "Banco Exemplo SA"
}

POST /v1/admin/psps/{id}/kyb/screenings/run

Screen with a provider

Screens the PSP and its contacts for sanctions and PEP with the provider (PSP-4.4); every result is stored as evidence and emitted.

Parameters

NameInTypeDescription
id requiredpathstring

Request body application/json required

FieldTypeDescription
provider requiredstring manual (creates review tasks) or a configured provider.

Responses

  • 201 The screenings.

Request

curl -X POST "$KULPAY_URL/v1/admin/psps/{id}/kyb/screenings/run" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "provider": "mpesa"
}'
201 Response application/json
{
  "data": [
    {
      "at": "2026-10-10T09:30:00Z",
      "evidence_file_id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
      "id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
      "kind": "sanctions",
      "notes": "Pagamento de energia, outubro",
      "performed_by": "string",
      "provider": "mpesa",
      "psp_id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
      "reference": "2003014567",
      "result": "clear",
      "subject": "string"
    }
  ]
}

GET /v1/admin/psps/{id}/offboarding

What offboarded waits for

Parameters

NameInTypeDescription
id requiredpathstring

Responses

  • 200 Gates.

Request

curl -X GET "$KULPAY_URL/v1/admin/psps/{id}/offboarding" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "gates": [
    {
      "detail": "the account takes no new payments",
      "met": true,
      "name": "routing_drained"
    },
    {
      "detail": "0 active key(s)",
      "met": true,
      "name": "credentials_revoked"
    }
  ],
  "status": "offboarding"
}

POST /v1/admin/psps/{id}/offboarding

Prepare offboarding (maker)

From pilot, live or suspended. On approval by a second person: no new payments, every key revoked, psp.offboarding.started.

Parameters

NameInTypeDescription
id requiredpathstring

Request body application/json required

FieldTypeDescription
reason requiredstring

Responses

  • 202 Prepared; a second person decides it (POST /v1/admin/approvals/{id}/decide).
  • 400 invalid_request

Request

curl -X POST "$KULPAY_URL/v1/admin/psps/{id}/offboarding" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "reason": "contract ended"
}'
202 Response application/json
{
  "approval": {
    "created_at": "2026-10-10T22:15:38.88584572Z",
    "id": "3KWPq6oHbsrNPhdipq2aUDNz3FJ",
    "kind": "psp.offboard",
    "prepared_by": "key_accounts",
    "reason": "contract ended",
    "status": "pending",
    "subject_id": "3KWPq84JWIgPA0bRlrInvqXVHZS",
    "tenant_id": "3KWPqAm6bUbJrAb7hW8f2ZU6pPT"
  },
  "message": "offboarding prepared; a second person must approve it"
}
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "invalid_request",
  "error": {
    "code": "invalid_request",
    "message": "invalid_request"
  },
  "status": 400,
  "title": "Bad Request",
  "type": "about:blank"
}

POST /v1/admin/psps/{id}/offboarding/complete

Complete offboarding

Once routing is drained, credentials revoked, live payments final and reconciliation closed: offboarded (psp.offboarded). Nothing is deleted.

Parameters

NameInTypeDescription
id requiredpathstring

Request body application/json required

FieldTypeDescription
reason requiredstring

Responses

Request

curl -X POST "$KULPAY_URL/v1/admin/psps/{id}/offboarding/complete" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "reason": "settled"
}'
200 Response application/json
{
  "account_id": "3KWPqAm6bUbJrAb7hW8f2ZU6pPT",
  "application_id": "3KWPq6zQflNIw3ra10OHzdZABgk",
  "contract_signed": false,
  "country": "MZ",
  "created_at": "2026-10-10T22:15:38.521919662Z",
  "id": "3KWPq84JWIgPA0bRlrInvqXVHZS",
  "kind": "psp",
  "kyb_status": "in_review",
  "language": "pt",
  "last_review_at": "2026-10-10T22:15:38.858035949Z",
  "legal_name": "Banco Exemplo SA",
  "next_review_at": "2027-04-10T22:15:38.858035949Z",
  "region": "",
  "registration_number": "100234567",
  "risk_tier": "high",
  "sandbox_enabled_at": "2026-10-10T22:15:38.605512701Z",
  "sandbox_key_id": "3KWPqBPUHHtpYIo3jN74t1OHRVj",
  "state_entered_at": "2026-10-10T22:15:38.950522963Z",
  "status": "offboarded",
  "updated_at": "2026-10-10T22:15:38.950522963Z"
}
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "Open items remain (invalid_request).",
  "error": {
    "code": "invalid_request",
    "message": "Open items remain (invalid_request)."
  },
  "status": 400,
  "title": "Bad Request",
  "type": "about:blank"
}

PUT /v1/admin/psps/{id}/pilot

Pilot limits, merchants and routing weight

In pilot only. Entering pilot sets 10% of the contracted limits, no merchants and a 10% weight. Live payments beyond the limits, or without an allowed metadata.merchant_id, are refused (422 gateway.pilot_limit).

Parameters

NameInTypeDescription
id requiredpathstring

Request body application/json required

FieldTypeDescription
currencystring
dailystring
merchantsstring[] Allow-list: a live payment in pilot must carry metadata.merchant_id from it.
monthlystring
per_transactionstring
reason requiredstring
routing_weightinteger % of eligible traffic routed to the PSP.

Responses

  • 200 The settings.
  • 400 Above the contract, or not in pilot (invalid_request).

Request

curl -X PUT "$KULPAY_URL/v1/admin/psps/{id}/pilot" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "daily": "100000",
  "merchants": [
    "m-1"
  ],
  "monthly": "2000000",
  "per_transaction": "40000",
  "reason": "first merchant",
  "routing_weight": 20
}'
200 Response application/json
{
  "currency": "MZN",
  "daily": "100000",
  "merchants": [
    "m-1"
  ],
  "monthly": "2000000",
  "per_transaction": "40000",
  "routing_weight": 20
}
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "per_transaction 500000 exceeds the contracted 400000",
  "error": {
    "code": "invalid_request",
    "message": "per_transaction 500000 exceeds the contracted 400000"
  },
  "status": 400,
  "title": "Bad Request",
  "type": "about:blank"
}

POST /v1/admin/psps/{id}/pilot/setup

Retry the pilot setup

If psp.pilot.setup_failed was recorded on entering pilot: sets the default pilot settings and issues the live key. Idempotent.

Parameters

NameInTypeDescription
id requiredpathstring

Responses

  • 200 The PSP.

Request

curl -X POST "$KULPAY_URL/v1/admin/psps/{id}/pilot/setup" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "account_id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
  "application_id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
  "certification_run_id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
  "certified_at": "2026-10-10T09:30:00Z",
  "contract_ref": "string",
  "contract_signed": true,
  "country": "MZ",
  "emergency_suspended_at": "2026-10-10T09:30:00Z",
  "id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
  "kyb_approved_at": "2026-10-10T09:30:00Z",
  "kyb_status": "pending",
  "language": "pt",
  "last_review_at": "2026-10-10T09:30:00Z",
  "legal_name": "Electricidade de Moçambique",
  "live_at": "2026-10-10T09:30:00Z",
  "live_key_id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
  "pilot_started_at": "2026-10-10T09:30:00Z",
  "region": "string",
  "registration_number": "string",
  "resume_state": "succeeded",
  "review_notice": "",
  "risk_tier": "string",
  "sandbox_enabled_at": "2026-10-10T09:30:00Z",
  "sandbox_key_id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
  "state_entered_at": "2026-10-10T09:30:00Z",
  "status": "applied",
  "suspension_review_id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
  "suspension_review_overdue": true
}

POST /v1/admin/psps/{id}/reinstate

Reinstate a suspended PSP

Back to the state it left, with its account status and keys restored. Records psp.reinstated.

Parameters

NameInTypeDescription
id requiredpathstring

Request body application/json required

FieldTypeDescription
reason requiredstring

Responses

  • 200 The PSP.

Request

curl -X POST "$KULPAY_URL/v1/admin/psps/{id}/reinstate" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "reason": "Pagamento de energia, outubro"
}'
200 Response application/json
{
  "account_id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
  "application_id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
  "certification_run_id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
  "certified_at": "2026-10-10T09:30:00Z",
  "contract_ref": "string",
  "contract_signed": true,
  "country": "MZ",
  "emergency_suspended_at": "2026-10-10T09:30:00Z",
  "id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
  "kyb_approved_at": "2026-10-10T09:30:00Z",
  "kyb_status": "pending",
  "language": "pt",
  "last_review_at": "2026-10-10T09:30:00Z",
  "legal_name": "Electricidade de Moçambique",
  "live_at": "2026-10-10T09:30:00Z",
  "live_key_id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
  "pilot_started_at": "2026-10-10T09:30:00Z",
  "region": "string",
  "registration_number": "string",
  "resume_state": "succeeded",
  "review_notice": "",
  "risk_tier": "string",
  "sandbox_enabled_at": "2026-10-10T09:30:00Z",
  "sandbox_key_id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
  "state_entered_at": "2026-10-10T09:30:00Z",
  "status": "applied",
  "suspension_review_id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
  "suspension_review_overdue": true
}

POST /v1/admin/psps/{id}/reviews

Complete a periodic review

Records psp.review.completed and schedules the next review by risk tier.

Parameters

NameInTypeDescription
id requiredpathstring

Request body application/json required

FieldTypeDescription
notes requiredstring
outcome requiredstring
One of: satisfactory, actions_required, escalated

Responses

  • 200 The PSP.

Request

curl -X POST "$KULPAY_URL/v1/admin/psps/{id}/reviews" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "notes": "licence current",
  "outcome": "satisfactory"
}'
200 Response application/json
{
  "account_id": "3KWPqAm6bUbJrAb7hW8f2ZU6pPT",
  "application_id": "3KWPq6zQflNIw3ra10OHzdZABgk",
  "contract_signed": false,
  "country": "MZ",
  "created_at": "2026-10-10T22:15:38.521919662Z",
  "id": "3KWPq84JWIgPA0bRlrInvqXVHZS",
  "kind": "psp",
  "kyb_status": "in_review",
  "language": "pt",
  "last_review_at": "2026-10-10T22:15:38.858035949Z",
  "legal_name": "Banco Exemplo SA",
  "next_review_at": "2027-04-10T22:15:38.858035949Z",
  "region": "",
  "registration_number": "100234567",
  "resume_state": "technical_setup",
  "risk_tier": "high",
  "sandbox_enabled_at": "2026-10-10T22:15:38.605512701Z",
  "sandbox_key_id": "3KWPqBPUHHtpYIo3jN74t1OHRVj",
  "state_entered_at": "2026-10-10T22:15:38.822160182Z",
  "status": "suspended",
  "updated_at": "2026-10-10T22:15:38.858035949Z"
}

POST /v1/admin/psps/{id}/sandbox

Open the sandbox

Opens the sandbox if that failed on entering technical_setup (psp.sandbox.enable_failed): test-mode mock bindings for every method and a test key the PSP reveals once on the portal. Idempotent. Records psp.sandbox.enabled. Requires accounts:write.

Parameters

NameInTypeDescription
id requiredpathstring

Responses

Request

curl -X POST "$KULPAY_URL/v1/admin/psps/{id}/sandbox" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "account_id": "3KWPnlM19nPUwG6nNuynNezp1w7",
  "application_id": "3KWPnhekPhfRi1KsQ8oQk3l33ma",
  "contract_signed": false,
  "country": "MZ",
  "created_at": "2026-10-10T22:15:19.816714735Z",
  "id": "3KWPnnBS0h6YDMR6gHeiAUNjVFo",
  "kind": "psp",
  "kyb_status": "in_review",
  "language": "pt",
  "legal_name": "Banco Exemplo SA",
  "region": "",
  "registration_number": "100234567",
  "sandbox_enabled_at": "2026-10-10T22:15:19.916518232Z",
  "sandbox_key_id": "3KWPnhHlWvZRFKddEUPjtBP6SFc",
  "state_entered_at": "2026-10-10T22:15:19.8755279Z",
  "status": "technical_setup",
  "updated_at": "2026-10-10T22:15:19.947987543Z"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "not_found",
  "error": {
    "code": "not_found",
    "message": "not_found"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}
409 Error · sandbox_unavailable application/problem+json
{
  "code": "sandbox_unavailable",
  "detail": "Not in technical_setup or later (sandbox_unavailable).",
  "error": {
    "code": "sandbox_unavailable",
    "message": "Not in technical_setup or later (sandbox_unavailable)."
  },
  "status": 409,
  "title": "Conflict",
  "type": "about:blank"
}

POST /v1/admin/psps/{id}/suspension

Suspend a PSP

Suspension blocks the PSP's account (no new payments, checkouts or links) and refuses its keys from the next request on every node; payments in flight run to their end. Normally prepared by one person and approved by another (202). With emergency, it takes effect at once (200) and creates a psp.suspension.review approval: upholding it keeps the suspension, rejecting it reinstates the PSP.

Parameters

NameInTypeDescription
id requiredpathstring

Request body application/json required

FieldTypeDescription
emergencyboolean Suspend now on one officer's word; a second reviews within 24 hours.
reason requiredstring

Responses

  • 200 Suspended (emergency).
  • 202 Prepared; a second person decides it (POST /v1/admin/approvals/{id}/decide).
  • 400 invalid_request

Request

curl -X POST "$KULPAY_URL/v1/admin/psps/{id}/suspension" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "emergency": true,
  "reason": "card testing"
}'
200 Response application/json
{
  "message": "suspended now; a second officer must review it within 24 hours",
  "psp": {
    "account_id": "3KWPqAm6bUbJrAb7hW8f2ZU6pPT",
    "application_id": "3KWPq6zQflNIw3ra10OHzdZABgk",
    "contract_signed": false,
    "country": "MZ",
    "created_at": "2026-10-10T22:15:38.521919662Z",
    "emergency_suspended_at": "2026-10-10T22:15:38.710738721Z",
    "id": "3KWPq84JWIgPA0bRlrInvqXVHZS",
    "kind": "psp",
    "kyb_status": "in_review",
    "language": "pt",
    "legal_name": "Banco Exemplo SA",
    "next_review_at": "2027-04-10T22:15:38.627684253Z",
    "region": "",
    "registration_number": "100234567",
    "resume_state": "technical_setup",
    "risk_tier": "high",
    "sandbox_enabled_at": "2026-10-10T22:15:38.605512701Z",
    "sandbox_key_id": "3KWPqBPUHHtpYIo3jN74t1OHRVj",
    "state_entered_at": "2026-10-10T22:15:38.709280961Z",
    "status": "suspended",
    "suspension_review_id": "3KWPqBtjd4R5ETH8OB7th9QWOjF",
    "updated_at": "2026-10-10T22:15:38.709280961Z"
  },
  "review": {
    "created_at": "2026-10-10T22:15:38.707281265Z",
    "id": "3KWPqBtjd4R5ETH8OB7th9QWOjF",
    "kind": "psp.suspension.review",
    "prepared_by": "key_accounts",
    "reason": "review the emergency suspension: card testing",
    "status": "pending",
    "subject_id": "3KWPq84JWIgPA0bRlrInvqXVHZS",
    "tenant_id": "3KWPqAm6bUbJrAb7hW8f2ZU6pPT"
  }
}
202 Response application/json
{
  "approval": {
    "created_at": "2026-10-10T22:15:38.802980169Z",
    "id": "3KWPq7pLTTBVFL4s8IbSgriXzE8",
    "kind": "psp.suspend",
    "prepared_by": "key_accounts",
    "reason": "SLA breaches",
    "status": "pending",
    "subject_id": "3KWPq84JWIgPA0bRlrInvqXVHZS",
    "tenant_id": "3KWPqAm6bUbJrAb7hW8f2ZU6pPT"
  },
  "message": "suspension prepared; a second person must approve it"
}
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "invalid_request",
  "error": {
    "code": "invalid_request",
    "message": "invalid_request"
  },
  "status": 400,
  "title": "Bad Request",
  "type": "about:blank"
}

GET /v1/admin/psps/{id}/technical

Sandbox technical setup

The sandbox (test key, mock connectors per method), live-key status and each setup step with its last validation: capability matrix, orientation profile, endpoints (TLS 1.2+ reachability), credentials (certificate validity and key, PGP key fingerprint), webhooks, IP allow-list and statement profile. The PSP completes the steps on the onboarding portal. Requires accounts:read.

Parameters

NameInTypeDescription
id requiredpathstring

Responses

Request

curl -X GET "$KULPAY_URL/v1/admin/psps/{id}/technical" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "complete": false,
  "keys": [
    {
      "account_id": "3KWPnlM19nPUwG6nNuynNezp1w7",
      "created_at": "2026-10-10T22:15:19.893892266Z",
      "format": "kp1",
      "id": "3KWPnhHlWvZRFKddEUPjtBP6SFc",
      "last4": "uQdZ",
      "mode": "test",
      "name": "sandbox",
      "scopes": [
        "payments:read",
        "payments:write"
      ],
      "type": "sk",
      "use_count": 0
    }
  ],
  "live_key_revealable": false,
  "live_keys_allowed": false,
  "psp_id": "3KWPnnBS0h6YDMR6gHeiAUNjVFo",
  "require_mtls": false,
  "sandbox_enabled": true,
  "sandbox_key_id": "3KWPnhHlWvZRFKddEUPjtBP6SFc",
  "sandbox_key_revealable": false,
  "sandbox_methods": [
    "card"
  ],
  "status": "technical_setup",
  "steps": [
    {
      "checked_at": "2026-10-10T22:15:20.116851131Z",
      "checks": [
        {
          "detail": "card; inbound, async, MZN",
          "name": "SALE",
          "result": "pass"
        }
      ],
      "config_kind": "capability_matrix",
      "config_version": 1,
      "status": "passed",
      "step": "capability_matrix",
      "value": {
        "types": [
          {
            "confirmation": "callback",
            "countries": [
              "MZ"
            ],
            "currencies": [
              "MZN"
            ],
            "direction": "inbound",
            "idempotency": false,
            "methods": [
              "card"
            ],
            "mode": "async",
            "partial": false,
            "three_ds": false,
            "timeout_ms": 30000,
            "type": "SALE"
          }
        ]
      }
    },
    {
      "checks": [],
      "config_kind": "orientation_profile",
      "status": "pending",
      "step": "orientation_profile"
    }
  ]
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "not_found",
  "error": {
    "code": "not_found",
    "message": "not_found"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

POST /v1/admin/psps/{id}/transitions

Move a PSP in its lifecycle

Only transitions in the declared table, with their gates met, are allowed; each records the actor and the reason and emits psp.<state>.entered. A refused one emits psp.transition.rejected. Requires accounts:write.

Parameters

NameInTypeDescription
id requiredpathstring

Request body application/json required

FieldTypeDescription
reason requiredstring
to requiredstring

Responses

  • 200 The PSP after the move.
  • 409 Not allowed (invalid_transition, with from, to and cause).

Request

curl -X POST "$KULPAY_URL/v1/admin/psps/{id}/transitions" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "reason": "go",
  "to": "kyb_review"
}'
200 Response application/json
{
  "account_id": "3KWPpsYfFNrQEhm84v3anWxHrHj",
  "application_id": "3KWPprbA3vQfJCZgfhOvbGOxKhO",
  "certified_at": "2026-10-10T22:15:36.187945473Z",
  "contract_signed": true,
  "country": "MZ",
  "created_at": "2026-10-10T22:15:36.187945473Z",
  "id": "3KWPpp00pI0ILGNVqqM3kUpSxNv",
  "kind": "psp",
  "kyb_status": "approved",
  "language": "pt",
  "legal_name": "Banco Exemplo SA",
  "live_key_id": "3KWPpr5y02e3qlgVBBep5PQmDrF",
  "next_review_at": "2027-10-10T22:15:36.187945473Z",
  "pilot_started_at": "2026-10-10T22:15:36.187945473Z",
  "region": "mz",
  "registration_number": "100234567",
  "risk_tier": "medium",
  "sandbox_enabled_at": "2026-10-10T22:15:36.187945473Z",
  "sandbox_key_id": "3KWPps54CGNg8OL8aTqvdGqMbZb",
  "state_entered_at": "2026-10-10T22:15:36.187945473Z",
  "status": "pilot",
  "updated_at": "2026-10-10T22:15:36.187945473Z"
}
409 Error · invalid_transition application/problem+json
{
  "cause": "suspension needs two people (POST /v1/admin/psps/{id}/suspension) or an emergency suspension",
  "code": "invalid_transition",
  "detail": "cannot move PSP from technical_setup to suspended: suspension needs two people (POST /v1/admin/psps/{id}/suspension) or an emergency suspension",
  "error": {
    "cause": "suspension needs two people (POST /v1/admin/psps/{id}/suspension) or an emergency suspension",
    "code": "invalid_transition",
    "from": "technical_setup",
    "message": "cannot move PSP from technical_setup to suspended: suspension needs two people (POST /v1/admin/psps/{id}/suspension) or an emergency suspension",
    "to": "suspended"
  },
  "from": "technical_setup",
  "status": 409,
  "title": "Conflict",
  "to": "suspended",
  "type": "about:blank"
}

POST /v1/psps/applications

Start a PSP application

Creates a draft. No API key: the response carries a draft_token, the key to this draft. Also available as the onboarding portal at /onboarding.

Request body application/json required

FieldTypeDescription
compliance_contactobject
compliance_contact.emailstring (email)
compliance_contact.namestring
compliance_contact.phonestring
countriesstring[]
countrystring
currenciesstring[]
expected_monthly_valueobject
expected_monthly_value.amountstring
expected_monthly_value.currencystring
expected_monthly_volumeinteger
languagestring
One of: pt, en
legal_namestring
licencesobject[]
licences[].expires_onstring (date)
licences[].issuerstring
licences[].numberstring
licences[].typestring
registration_numberstring
rolesstring[]
technical_contactobject
technical_contact.emailstring (email)
technical_contact.namestring
technical_contact.phonestring
transaction_typesstring[]

Responses

  • 201 The draft, with what is still missing and its token.
  • 400 Malformed or unknown fields (invalid_request).

Request

curl -X POST "$KULPAY_URL/v1/psps/applications" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "compliance_contact": {
    "email": "ops@example.co.mz",
    "name": "Rui"
  },
  "countries": [
    "MZ"
  ],
  "country": "MZ",
  "currencies": [
    "MZN"
  ],
  "expected_monthly_value": {
    "amount": "1000000",
    "currency": "MZN"
  },
  "expected_monthly_volume": 5000,
  "language": "pt",
  "legal_name": "Banco Exemplo SA",
  "licences": [
    {
      "issuer": "Banco de Moçambique",
      "number": "BM-PI-0042",
      "type": "payment_institution"
    }
  ],
  "registration_number": "100234567",
  "roles": [
    "processor"
  ],
  "technical_contact": {
    "email": "ops@example.co.mz",
    "name": "Ana"
  },
  "transaction_types": [
    "mobile_money_c2b"
  ]
}'
201 Response application/json
{
  "data": {
    "compliance_contact": {
      "email": "ops@example.co.mz",
      "name": "Rui"
    },
    "countries": [
      "MZ"
    ],
    "country": "MZ",
    "currencies": [
      "MZN"
    ],
    "expected_monthly_value": {
      "amount": "1000000",
      "currency": "MZN"
    },
    "expected_monthly_volume": 5000,
    "language": "pt",
    "legal_name": "Banco Exemplo SA",
    "licences": [
      {
        "issuer": "Banco de Moçambique",
        "number": "BM-PI-0042",
        "type": "payment_institution"
      }
    ],
    "registration_number": "100234567",
    "roles": [
      "processor"
    ],
    "technical_contact": {
      "email": "ops@example.co.mz",
      "name": "Ana"
    },
    "transaction_types": [
      "mobile_money_c2b"
    ]
  },
  "draft_token": "pspapp_8eae512582741e86ae8eb28b3730d8012ed13c6c64f5e6e7",
  "id": "3KWPlM2cQ5XsyMFrtw1MgS2gcZP",
  "language": "pt",
  "missing": [],
  "status": "draft"
}
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "Malformed or unknown fields (invalid_request).",
  "error": {
    "code": "invalid_request",
    "message": "Malformed or unknown fields (invalid_request)."
  },
  "status": 400,
  "title": "Bad Request",
  "type": "about:blank"
}

GET /v1/psps/applications/{id}

Read an application

Parameters

NameInTypeDescription
id requiredpathstring
KulPay-Application-Token requiredheaderstring

Responses

  • 200 The application.
  • 404 No such application, or the wrong token (not_found).

Request

curl -X GET "$KULPAY_URL/v1/psps/applications/{id}" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "data": {
    "compliance_contact": {
      "email": "ops@example.co.mz",
      "name": "Electricidade de Moçambique",
      "phone": "+258841234567"
    },
    "countries": [
      "string"
    ],
    "country": "MZ",
    "currencies": [
      "string"
    ],
    "expected_monthly_value": {
      "amount": "1250.00",
      "currency": "MZN"
    },
    "expected_monthly_volume": 12,
    "language": "pt",
    "legal_name": "Electricidade de Moçambique",
    "licences": [
      {
        "expires_on": "2026-10-31",
        "issuer": "string",
        "number": "string",
        "type": "standard"
      }
    ],
    "registration_number": "string",
    "roles": [
      "processor"
    ],
    "technical_contact": {
      "email": "ops@example.co.mz",
      "name": "Electricidade de Moçambique",
      "phone": "+258841234567"
    },
    "transaction_types": [
      "string"
    ]
  },
  "draft_token": "tok_7Hq2…",
  "id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
  "language": "pt",
  "missing": [
    "string"
  ],
  "psp_id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
  "status": "draft"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "application not found",
  "error": {
    "code": "not_found",
    "message": "application not found"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

PUT /v1/psps/applications/{id}

Save the draft

Parameters

NameInTypeDescription
id requiredpathstring
KulPay-Application-Token requiredheaderstring

Request body application/json required

FieldTypeDescription
compliance_contactobject
compliance_contact.emailstring (email)
compliance_contact.namestring
compliance_contact.phonestring
countriesstring[]
countrystring
currenciesstring[]
expected_monthly_valueobject
expected_monthly_value.amountstring
expected_monthly_value.currencystring
expected_monthly_volumeinteger
languagestring
One of: pt, en
legal_namestring
licencesobject[]
licences[].expires_onstring (date)
licences[].issuerstring
licences[].numberstring
licences[].typestring
registration_numberstring
rolesstring[]
technical_contactobject
technical_contact.emailstring (email)
technical_contact.namestring
technical_contact.phonestring
transaction_typesstring[]

Responses

Request

curl -X PUT "$KULPAY_URL/v1/psps/applications/{id}" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "compliance_contact": {
    "email": "ops@example.co.mz",
    "name": "Rui"
  },
  "countries": [
    "MZ"
  ],
  "country": "MZ",
  "currencies": [
    "MZN"
  ],
  "expected_monthly_value": {
    "amount": "1000000",
    "currency": "MZN"
  },
  "expected_monthly_volume": 5000,
  "language": "pt",
  "legal_name": "Banco Exemplo SA",
  "licences": [
    {
      "issuer": "Banco de Moçambique",
      "number": "BM-PI-0042",
      "type": "payment_institution"
    }
  ],
  "registration_number": "100234567",
  "roles": [
    "processor"
  ],
  "technical_contact": {
    "email": "ops@example.co.mz",
    "name": "Ana"
  },
  "transaction_types": [
    "mobile_money_c2b"
  ]
}'
200 Response application/json
{
  "data": {
    "compliance_contact": {
      "email": "ops@example.co.mz",
      "name": "Rui"
    },
    "countries": [
      "MZ"
    ],
    "country": "MZ",
    "currencies": [
      "MZN"
    ],
    "expected_monthly_value": {
      "amount": "1000000",
      "currency": "MZN"
    },
    "expected_monthly_volume": 5000,
    "language": "pt",
    "legal_name": "Banco Exemplo SA",
    "licences": [
      {
        "issuer": "Banco de Moçambique",
        "number": "BM-PI-0042",
        "type": "payment_institution"
      }
    ],
    "registration_number": "100234567",
    "roles": [
      "processor"
    ],
    "technical_contact": {
      "email": "ops@example.co.mz",
      "name": "Ana"
    },
    "transaction_types": [
      "mobile_money_c2b"
    ]
  },
  "id": "3KWPqEOIKJcrcDVHzmltGAVUEqA",
  "language": "pt",
  "missing": [],
  "status": "draft"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "not_found",
  "error": {
    "code": "not_found",
    "message": "not_found"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}
409 Error · already_submitted application/problem+json
{
  "code": "already_submitted",
  "detail": "Already submitted (already_submitted).",
  "error": {
    "code": "already_submitted",
    "message": "Already submitted (already_submitted)."
  },
  "status": 409,
  "title": "Conflict",
  "type": "about:blank"
}

POST /v1/psps/applications/{id}/documents

Upload a KYB document (applicant)

After submission, the applicant uploads its KYB documents with its draft token. Stored encrypted in the region's object storage with a checksum; records psp.kyb.document.uploaded.

Parameters

NameInTypeDescription
id requiredpathstring
KulPay-Application-Token requiredheaderstring

Request body multipart/form-data required

FieldTypeDescription
file requiredstring (binary) Up to 10 MB.
type requiredstring
One of: licence, registration_certificate, articles, ubo_declaration, director_ids, aml_policy, financial_statements

Responses

Request

curl -X POST "$KULPAY_URL/v1/psps/applications/{id}/documents" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -F "file=@./document.pdf"
201 Response application/json
{
  "file_id": "3KWPq4JEwiZkjmlc4D4BQL6xx0j",
  "filename": "licence.pdf",
  "id": "3KWPq3VocBtK5YJsaPSzJXtujyD",
  "psp_id": "3KWPpxo3jWfN94PGtqWEXcMLDgp",
  "sha256": "4cff4e3de93bfed4899f4cbfc7be8b77cdafec9560a995cd6092c00c8ba75302",
  "status": "pending",
  "type": "licence",
  "uploaded_at": "2026-10-10T22:15:37.433724445Z"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "application not found",
  "error": {
    "code": "not_found",
    "message": "application not found"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}
409 Error · not_submitted application/problem+json
{
  "code": "not_submitted",
  "detail": "Not submitted yet (not_submitted).",
  "error": {
    "code": "not_submitted",
    "message": "Not submitted yet (not_submitted)."
  },
  "status": 409,
  "title": "Conflict",
  "type": "about:blank"
}

POST /v1/psps/applications/{id}/submit

Submit the application

Creates the PSP in state applied as a tenant of the region its country belongs to, and sends a confirmation to its contacts in their language. Records psp.application.submitted and psp.applied.entered.

Parameters

NameInTypeDescription
id requiredpathstring
KulPay-Application-Token requiredheaderstring

Responses

Request

curl -X POST "$KULPAY_URL/v1/psps/applications/{id}/submit" \
  -H "Authorization: Bearer $KULPAY_KEY"
201 Response application/json
{
  "psp_id": "3KWPlMun761v0d6dW1NtuPMxddi",
  "region": "",
  "status": "applied"
}
409 Error · already_submitted application/problem+json
{
  "code": "already_submitted",
  "detail": "the application was already submitted",
  "error": {
    "code": "already_submitted",
    "message": "the application was already submitted"
  },
  "status": 409,
  "title": "Conflict",
  "type": "about:blank"
}
421 Error · wrong_region application/problem+json
{
  "base_url": "https://sa.api.kulpay.com",
  "code": "wrong_region",
  "detail": "This tenant's data is held in region regional-sa; send the request to https://sa.api.kulpay.com.",
  "error": {
    "base_url": "https://sa.api.kulpay.com",
    "code": "wrong_region",
    "message": "This tenant's data is held in region regional-sa; send the request to https://sa.api.kulpay.com.",
    "region": "regional-sa"
  },
  "region": "regional-sa",
  "status": 421,
  "title": "Misdirected Request",
  "type": "about:blank"
}
422 Error · application_incomplete application/problem+json
{
  "code": "application_incomplete",
  "detail": "the application is missing required fields",
  "error": {
    "code": "application_incomplete",
    "message": "the application is missing required fields",
    "missing": [
      "registration_number",
      "country"
    ]
  },
  "missing": [
    "registration_number",
    "country"
  ],
  "status": 422,
  "title": "Unprocessable Content",
  "type": "about:blank"
}

GET /v1/psps/{id}/capabilities

Your capability matrix

For the PSP the key belongs to (scope psp:configure): the active matrix and version, a pending one if any, and the connector capability flags (refunds, partial_refunds, voids, 3ds) generated from it, by method.

Parameters

NameInTypeDescription
id requiredpathstring

Responses

  • 200 The matrix.
  • 404 Not your PSP (not_found).

Request

curl -X GET "$KULPAY_URL/v1/psps/{id}/capabilities" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "pending": {
    "types": [
      {
        "confirmation": "both",
        "countries": [
          "MZ"
        ],
        "currencies": [
          "MZN"
        ],
        "direction": "inbound",
        "expected_latency_ms": 1500,
        "idempotency": true,
        "max_amount": "100000",
        "methods": [
          "card"
        ],
        "min_amount": "1",
        "mode": "async",
        "partial": false,
        "three_ds": true,
        "timeout_ms": 20000,
        "type": "SALE"
      }
    ]
  },
  "pending_version": 1,
  "psp_id": "3KWPlauKgv7rrVi1Wsue46ebEDF"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "PSP not found",
  "error": {
    "code": "not_found",
    "message": "PSP not found"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

PUT /v1/psps/{id}/capabilities

Change your capability matrix

Creates a pending version (config.capability_matrix.version_created) that KulPay approves. For a PSP in pilot or live, a version that adds types is approved only after a certification run of that version passes; the suite runs on the pending version. Scope psp:configure.

Parameters

NameInTypeDescription
id requiredpathstring

Request body application/json required

FieldTypeDescription
reason requiredstring
typesobject[]
types[].confirmationstring
One of: callback, polling, both
types[].countriesstring[] ISO 3166 alpha-2; default the PSP's country.
types[].currenciesstring[]
types[].daily_limitstring
types[].directionstring Default by type (PAYOUT outbound, TRANSFER both, else inbound).
One of: inbound, outbound, both
types[].expected_latency_msinteger
types[].idempotencyboolean
types[].max_amountstring
types[].methodsstring[]
types[].min_amountstring
types[].modestring
One of: sync, async
types[].monthly_limitstring
types[].partialboolean Partial capture or refund allowed.
types[].patternstring How the interaction happened (PSP-7.1); absent for internal work.
One of: outbound_sync, outbound_async, outbound_notification, inbound_request, inbound_callback, inbound_file, outbound_file, status_query
types[].refund_window_daysinteger
types[].three_dsboolean
types[].timeout_msinteger
types[].type requiredstring A code of GET /v1/transaction-types, once per matrix.

Responses

Request

curl -X PUT "$KULPAY_URL/v1/psps/{id}/capabilities" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "reason": "first matrix",
  "types": [
    {
      "confirmation": "both",
      "countries": [
        "MZ"
      ],
      "currencies": [
        "MZN"
      ],
      "expected_latency_ms": 1500,
      "idempotency": true,
      "max_amount": "100000",
      "methods": [
        "card"
      ],
      "min_amount": "1",
      "partial": false,
      "three_ds": true,
      "timeout_ms": 20000,
      "type": "SALE"
    }
  ]
}'
202 Response application/json
{
  "approval": {
    "created_at": "2026-10-10T22:15:02.732993386Z",
    "id": "3KWPlejDpniZIMJi2oDCy8F9wj4",
    "kind": "config.activate",
    "payload": "{\"version_id\":\"3KWPlcQzdNHk683phJtjBHuTiUO\",\"kind\":\"capability_matrix\",\"version\":1}",
    "prepared_by": "3KWPlf5E3HSh19wzHUJIxMjitjS",
    "reason": "capability_matrix v1: first matrix",
    "status": "pending",
    "subject_id": "3KWPlauKgv7rrVi1Wsue46ebEDF",
    "tenant_id": "3KWPlZbqGHdlsM4XZrsJ7t1bZLx"
  },
  "message": "capability matrix version prepared; KulPay approves it before it is active",
  "version": {
    "approval_id": "3KWPlejDpniZIMJi2oDCy8F9wj4",
    "created_at": "2026-10-10T22:15:02.730855834Z",
    "created_by": "3KWPlf5E3HSh19wzHUJIxMjitjS",
    "id": "3KWPlcQzdNHk683phJtjBHuTiUO",
    "kind": "capability_matrix",
    "psp_id": "3KWPlauKgv7rrVi1Wsue46ebEDF",
    "reason": "first matrix",
    "status": "pending",
    "version": 1
  }
}
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "capability matrix: CAPTURE: needs one of AUTH enabled",
  "error": {
    "code": "invalid_request",
    "message": "capability matrix: CAPTURE: needs one of AUTH enabled"
  },
  "status": 400,
  "title": "Bad Request",
  "type": "about:blank"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "Not your PSP (not_found).",
  "error": {
    "code": "not_found",
    "message": "Not your PSP (not_found)."
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

GET /v1/psps/{id}/commissions

My commissions as a participant

What KulPay owes the PSP for its part in others' payments in a month (IO-4.4): each succeeded bill payment it took part in (initiating_psp, from_psp, to_psp) whose role a rule of its fee schedule prices (rule role), the amount posted to its balance with the payment as the reference, and the totals per currency. month is YYYY-MM (this month by default). Requires psp:configure.

Parameters

NameInTypeDescription
id requiredpathstring
monthquerystring

Responses

  • 200 The statement.

Request

curl -X GET "$KULPAY_URL/v1/psps/{id}/commissions" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "commissions": [
    {
      "account_id": "3KXgACCkulOffice0000000001",
      "amount_minor": 1250,
      "base_minor": 125000,
      "created_at": "2026-10-15T10:00:00Z",
      "currency": "MZN",
      "id": "3KXgCOM00000000000000000001",
      "mode": "live",
      "payment_id": "3KXgBILL0000000000000000001",
      "payment_kind": "bill_payment",
      "psp_id": "3KXgPSPkulOffice00000000001",
      "role": "initiating_psp",
      "rule": 0,
      "schedule_id": "3KXgFEE00000000000000000001",
      "schedule_version": 2
    }
  ],
  "mode": "live",
  "month": "2026-10",
  "psp_id": "3KXgPSPkulOffice00000000001",
  "totals": {
    "MZN": "12.50"
  }
}

POST /v1/psps/{id}/credit-reconciliations

Report my reconciliation of a day

The resolver's result for a day (IO-6.2): its ledger and settled totals, the credits KulPay applied it cannot find (missing_at_kuloffice, credit IDs) and its ledger postings KulPay did not apply (missing_at_kulpay). The day closes when KulPay's applied total and both totals agree and nothing is missing; each difference is an exception with an owner (IO-6.3), and one open for 48 hours alerts both treasuries (partner.alert). Sent again, the day is recorded again; an exception is never raised twice. Requires psp:configure.

Parameters

NameInTypeDescription
id requiredpathstring

Request body application/json required

FieldTypeDescription
currency requiredstring
date requiredstring
ledger_total requiredstring
missing_at_kulofficestring[]
missing_at_kulpayobject[]
missing_at_kulpay[].amountstring
missing_at_kulpay[].referencestring
missing_at_kulpay[].transaction_idstring
modestring
settled_total requiredstring

Responses

  • 200 The day as recorded.

Request

curl -X POST "$KULPAY_URL/v1/psps/{id}/credit-reconciliations" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "currency": "MZN",
  "date": "2026-10-14",
  "ledger_total": "500.00",
  "missing_at_kuloffice": [],
  "missing_at_kulpay": [],
  "settled_total": "500.00"
}'
200 Response application/json
{
  "applied_minor": 50000,
  "currency": "MZN",
  "date": "2026-10-14",
  "id": "3KXgREC00000000000000000001",
  "ledger_minor": 50000,
  "missing_at_kulpay": 0,
  "missing_at_resolver": 0,
  "mode": "live",
  "reported_at": "2026-10-15T02:00:00Z",
  "resolver_psp_id": "3KXgPSPkulOffice00000000001",
  "settled_minor": 50000,
  "status": "closed"
}

GET /v1/psps/{id}/credit-report

My credits of a day

A resolver's credits applied on a day (IO-6.2), yesterday by default: deposit, credit, amount, status, when applied, the resolver's transaction and the settlement batch that paid it, with the totals per currency. ?format=csv answers CSV. For the resolver itself, in the key's mode. Requires psp:configure.

Parameters

NameInTypeDescription
id requiredpathstring
datequerystring
formatquerystring

Responses

  • 200 The report.

Request

curl -X GET "$KULPAY_URL/v1/psps/{id}/credit-report" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "credits": [
    {
      "amount": "500.00",
      "applied_at": "2026-10-14T09:12:00Z",
      "credit_id": "3KXgCRD00000000000000000001",
      "currency": "MZN",
      "deposit_id": "3KXgDEP00000000000000000001",
      "settlement_batch_id": "3KXgBAT00000000000000000001",
      "status": "applied",
      "transaction_id": "txn_blnk_1"
    }
  ],
  "date": "2026-10-14",
  "mode": "live",
  "resolver_psp_id": "3KXgPSPkulOffice00000000001",
  "totals": {
    "MZN": "500.00"
  }
}

GET /v1/psps/{id}/keys

Transport keys

The PSP's active SSH and PGP public keys. Requires psp:configure.

Parameters

NameInTypeDescription
id requiredpathstring

Responses

  • 200 {data: [key], total}.

Request

curl -X GET "$KULPAY_URL/v1/psps/{id}/keys" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "data": [
    {
      "created_at": "2026-10-10T22:15:49.010888336Z",
      "created_by": "3KWPrQT3b9J2gFndeaFBcOpkzZk",
      "fingerprint": "SHA256:FtlACmaRUkSpmcOioTf/fQ1zdkUFFp81k4cVSg3MHH4",
      "id": "3KWPrVqAItzPWymXFh1TU7sxJ8S",
      "kind": "ssh",
      "psp_id": "3KWPrLbezXsS8uP4YCxyxiQdz0U",
      "public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMHN9XlAWufAHy8BVF3iELDlHS3d9XVcW+KzkS89F0OU"
    }
  ],
  "total": 1
}

POST /v1/psps/{id}/keys

Register an SSH or PGP public key

ssh: an authorized_keys line (Ed25519, ECDSA or RSA), the only way into KulPay-hosted SFTP. pgp: an ASCII-armored public key verifying the PSP's statement signatures. Records credential.psp_key.registered. Requires psp:configure.

Parameters

NameInTypeDescription
id requiredpathstring

Request body application/json required

FieldTypeDescription
commentstring
kind requiredstring
One of: ssh, pgp
public_key requiredstring

Responses

  • 201 The key with its fingerprint.
  • 400 Not a valid key, or already registered. invalid_request

Request

curl -X POST "$KULPAY_URL/v1/psps/{id}/keys" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "kind": "ssh",
  "public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMHN9XlAWufAHy8BVF3iELDlHS3d9XVcW+KzkS89F0OU"
}'
201 Response application/json
{
  "created_at": "2026-10-10T22:15:49.010888336Z",
  "created_by": "3KWPrQT3b9J2gFndeaFBcOpkzZk",
  "fingerprint": "SHA256:FtlACmaRUkSpmcOioTf/fQ1zdkUFFp81k4cVSg3MHH4",
  "id": "3KWPrVqAItzPWymXFh1TU7sxJ8S",
  "kind": "ssh",
  "psp_id": "3KWPrLbezXsS8uP4YCxyxiQdz0U",
  "public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMHN9XlAWufAHy8BVF3iELDlHS3d9XVcW+KzkS89F0OU"
}
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "public_key is not an SSH public key (authorized_keys format)",
  "error": {
    "code": "invalid_request",
    "message": "public_key is not an SSH public key (authorized_keys format)"
  },
  "status": 400,
  "title": "Bad Request",
  "type": "about:blank"
}

DELETE /v1/psps/{id}/keys/{key_id}

Revoke a transport key

Stops working at once. Records credential.psp_key.revoked. Requires psp:configure.

Parameters

NameInTypeDescription
id requiredpathstring
key_id requiredpathstring

Responses

  • 204 Revoked.
  • 404 No such active key. not_found

Request

curl -X DELETE "$KULPAY_URL/v1/psps/{id}/keys/{key_id}" \
  -H "Authorization: Bearer $KULPAY_KEY"
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such active key",
  "error": {
    "code": "not_found",
    "message": "no such active key"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

GET /v1/psps/{id}/orientation

A PSP's orientation profile

The interaction patterns declared for each transaction type the capability matrix enables (PSP-7.1), e.g. SALE = outbound_async + inbound_callback + status_query, the profile the matrix suggests, and the pattern vocabulary. Every event carries its direction and pattern; certification runs one case per declared pattern. Requires psp:configure.

Parameters

NameInTypeDescription
id requiredpathstring

Responses

  • 200 {psp_id, active, active_version, suggested, patterns}.

Request

curl -X GET "$KULPAY_URL/v1/psps/{id}/orientation" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "active": {
    "types": [
      {
        "patterns": [
          "inbound_callback",
          "outbound_async",
          "status_query"
        ],
        "type": "SALE"
      },
      {
        "patterns": [
          "outbound_async",
          "status_query"
        ],
        "type": "PAYOUT"
      }
    ]
  },
  "active_version": 2,
  "patterns": [
    "outbound_sync",
    "outbound_async",
    "outbound_notification",
    "inbound_request",
    "inbound_callback",
    "inbound_file",
    "outbound_file",
    "status_query"
  ],
  "psp_id": "3KbHx5Qp7eYLffSWnjTF10m1LdZ",
  "suggested": {
    "types": [
      {
        "patterns": [
          "inbound_callback",
          "outbound_async",
          "status_query"
        ],
        "type": "SALE"
      },
      {
        "patterns": [
          "inbound_callback",
          "outbound_async",
          "status_query"
        ],
        "type": "PAYOUT"
      }
    ]
  }
}

PUT /v1/psps/{id}/orientation

Declare a PSP's orientation profile

A new version, active at once (event config.orientation_profile.version_created). Types must be enabled in the capability matrix; patterns from the vocabulary.

Parameters

NameInTypeDescription
id requiredpathstring

Request body application/json required

FieldTypeDescription
reason requiredstring
types requiredobject[]
types[].patternsstring[]
types[].typestring

Responses

  • 201 The version.
  • 400 Invalid profile, each problem listed. invalid_request

Request

curl -X PUT "$KULPAY_URL/v1/psps/{id}/orientation" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "reason": "Pagamento de energia, outubro",
  "types": [
    {
      "patterns": [
        "outbound_sync"
      ],
      "type": "SALE"
    }
  ]
}'
201 Response application/json
{
  "version": {
    "activated_at": "2026-10-11T10:31:00Z",
    "created_at": "2026-10-11T10:31:00Z",
    "created_by": "3KIhzpLdNupTuLgu8bwZOQspar2",
    "id": "3K7OyLy19t8ti6E5r103HLquECm",
    "kind": "orientation_profile",
    "psp_id": "3KbHx5Qp7eYLffSWnjTF10m1LdZ",
    "reason": "SALE confirmed by callback, status query as fallback",
    "status": "active",
    "version": 2
  }
}
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "orientation profile: REFUND is not enabled in the capability matrix; SALE: unknown pattern callback (one of outbound_sync, outbound_async, outbound_notification, inbound_request, inbound_callback, inbound_file, outbound_file, status_query)",
  "error": {
    "code": "invalid_request",
    "message": "orientation profile: REFUND is not enabled in the capability matrix; SALE: unknown pattern callback (one of outbound_sync, outbound_async, outbound_notification, inbound_request, inbound_callback, inbound_file, outbound_file, status_query)"
  },
  "status": 400,
  "title": "Bad Request",
  "type": "about:blank"
}

GET /v1/psps/{id}/retry-policy

A PSP's effective retry policies

Per interaction pattern (PSP-9.2): retryable and never-retried conditions, delays (with jitter), maximum attempts and the action after the last attempt (failover, status_query, dlq, fail) — the PSP's active retry_policy version where it overrides a pattern, else the platform default — and the platform's limits. An unknown outcome is never resent: it goes to status queries. Requires psp:configure.

Parameters

NameInTypeDescription
id requiredpathstring

Responses

  • 200 {psp_id, active_version, effective: [policy], overridden: [pattern], limits}.

Request

curl -X GET "$KULPAY_URL/v1/psps/{id}/retry-policy" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "active_version": 3,
  "effective": [
    {
      "delays": [
        "0s"
      ],
      "jitter": 0,
      "max_attempts": 3,
      "never_on": [
        "hard_decline",
        "timeout"
      ],
      "on_exhausted": "fail",
      "pattern": "outbound_sync",
      "retry_on": [
        "error_before_send",
        "soft_decline"
      ]
    },
    {
      "delays": [
        "0s"
      ],
      "jitter": 0,
      "max_attempts": 3,
      "never_on": [
        "hard_decline",
        "timeout"
      ],
      "on_exhausted": "fail",
      "pattern": "outbound_async",
      "retry_on": [
        "error_before_send",
        "soft_decline"
      ]
    },
    {
      "delays": [
        "10s",
        "30s",
        "2m",
        "10m",
        "1h"
      ],
      "jitter": 0.1,
      "max_attempts": 0,
      "on_exhausted": "dlq",
      "pattern": "status_query",
      "retry_on": [
        "pending",
        "timeout",
        "connection_error",
        "http_5xx",
        "http_429"
      ],
      "timeout": "30s",
      "window": "48h"
    },
    {
      "delays": [
        "1m",
        "4m",
        "10m",
        "45m",
        "2h",
        "5h",
        "16h"
      ],
      "jitter": 0.1,
      "max_attempts": 8,
      "never_on": [
        "http_410",
        "http_4xx"
      ],
      "on_exhausted": "dlq",
      "pattern": "outbound_notification",
      "retry_on": [
        "timeout",
        "connection_error",
        "http_5xx",
        "http_429",
        "http_408"
      ]
    },
    {
      "delays": [
        "30s",
        "1m",
        "2m",
        "4m",
        "8m",
        "16m",
        "32m"
      ],
      "jitter": 0.2,
      "max_attempts": 8,
      "on_exhausted": "dlq",
      "pattern": "inbound_callback",
      "retry_on": [
        "timeout",
        "connection_error",
        "http_5xx"
      ]
    }
  ],
  "limits": {
    "conditions": [
      "error_before_send",
      "timeout",
      "connection_error",
      "http_5xx",
      "http_429",
      "http_408",
      "http_410",
      "http_4xx",
      "soft_decline",
      "hard_decline",
      "pending",
      "unknown_outcome"
    ],
    "max_attempts": 20,
    "max_delay": "24h0m0s",
    "max_jitter": 0.5,
    "max_total": "72h0m0s",
    "min_delay": "1s",
    "on_exhausted": [
      "failover",
      "status_query",
      "dlq",
      "fail"
    ]
  },
  "overridden": [
    "status_query"
  ],
  "psp_id": "3KbHx5Qp7eYLffSWnjTF10m1LdZ"
}

PUT /v1/psps/{id}/retry-policy

Tune retries, timeouts and circuits

A new version of the PSP's retry_policy (PSP-9.7): per pattern and optionally per transaction type, the conditions, delays, max_attempts, on_exhausted, a call timeout (1 s to 2 min) and, for status_query, the window (1 min to 72 h); breaker thresholds and the retry budget. Values outside the platform's limits are refused with the allowed range (GET .../retry-policy lists the limits). Versions are audited (config.retry_policy.version_created); a change to a money-moving pattern (outbound_sync, outbound_async) waits for KulPay's approval (202), any other is active at once (201). Requires psp:configure.

Parameters

NameInTypeDescription
id requiredpathstring

Request body application/json required

FieldTypeDescription
breakerobject
budgetobject
policiesobject[]
reason requiredstring

Responses

  • 201 Active at once.
  • 202 Waiting for KulPay's approval.
  • 400 Outside the limits; the allowed range is named. invalid_request

Request

curl -X PUT "$KULPAY_URL/v1/psps/{id}/retry-policy" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "breaker": {},
  "budget": {},
  "policies": [
    {}
  ],
  "reason": "Pagamento de energia, outubro"
}'
201 Response application/json
{
  "approval": null,
  "version": {
    "activated_at": "2026-10-11T10:14:00Z",
    "created_at": "2026-10-11T10:14:00Z",
    "created_by": "3KIhzpLdNupTuLgu8bwZOQspar2",
    "id": "3KXU64pBgOZqnJAQ4TXU6EIrN5F",
    "kind": "retry_policy",
    "psp_id": "3KbHx5Qp7eYLffSWnjTF10m1LdZ",
    "reason": "Longer status-query window for overnight M-Pesa settlement",
    "status": "active",
    "version": 3
  }
}
202 Response application/json
{
  "approval": {
    "created_at": "2026-10-11T10:20:00Z",
    "id": "3Ke5KHiog5p9N2Mxo29S5Szrbhy",
    "kind": "config.activate",
    "payload": "{\"version_id\":\"3Kpqk5QKsEzH7qwjFpKRodpoJ4G\",\"kind\":\"retry_policy\",\"version\":4}",
    "prepared_by": "3KIhzpLdNupTuLgu8bwZOQspar2",
    "reason": "retry_policy v4: Fail over after two soft declines instead of three",
    "status": "pending",
    "subject_id": "3KbHx5Qp7eYLffSWnjTF10m1LdZ",
    "tenant_id": "3K12JLy9OnIwuN9yzoIgIKQpHJX"
  },
  "version": {
    "approval_id": "3Ke5KHiog5p9N2Mxo29S5Szrbhy",
    "created_at": "2026-10-11T10:20:00Z",
    "created_by": "3KIhzpLdNupTuLgu8bwZOQspar2",
    "id": "3Kpqk5QKsEzH7qwjFpKRodpoJ4G",
    "kind": "retry_policy",
    "psp_id": "3KbHx5Qp7eYLffSWnjTF10m1LdZ",
    "reason": "Fail over after two soft declines instead of three",
    "status": "pending",
    "version": 4
  }
}
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "retry policy: status_query: window \"96h\" must be between 1m0s and 72h0m0s; outbound_notification: max_attempts must be between 1 and 20 (0, until the window closes, only for status_query)",
  "error": {
    "code": "invalid_request",
    "message": "retry policy: status_query: window \"96h\" must be between 1m0s and 72h0m0s; outbound_notification: max_attempts must be between 1 and 20 (0, until the window closes, only for status_query)"
  },
  "status": 400,
  "title": "Bad Request",
  "type": "about:blank"
}

GET /v1/psps/{id}/statement-endpoints

Where to send statements

KulPay-hosted SFTP (address, user = the PSP ID, host key and its SHA-256 fingerprint), KulPay's PGP public key for this region, the upload path and the per-entry webhook path. Over SFTP the PSP writes into its own write-only folder; a file is matched to its sftp_kulpay profile by name, and where a checksum is required a <file>.sha256 sidecar carries it. Requires psp:configure.

Parameters

NameInTypeDescription
id requiredpathstring

Responses

  • 200 {sftp_address, sftp_user, sftp_host_key, sftp_host_key_fingerprint, pgp_public_key, pgp_fingerprint, upload_path, webhook_path}.

Request

curl -X GET "$KULPAY_URL/v1/psps/{id}/statement-endpoints" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "pgp_fingerprint": "E2D9CD60195D1C56D1A202291C52F0688B5AD15D",
  "pgp_public_key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nxsDNBGrKuRUBDACshel2oJCvIWi4VIi5ddjpl300qL+ZtSSP+EEX6yASuNCMetA1\naCjNYvGWoGfjokgcW…",
  "sftp_host_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJipMDgGetJsqh64QKWkI8QsHW9Fea1BPu7N/8tZ/QVl",
  "sftp_host_key_fingerprint": "SHA256:U0tDE+26h2y7Nk42LWqWemUfppilRPFYOv1nKlKE6tM",
  "sftp_user": "3KWPrLbezXsS8uP4YCxyxiQdz0U",
  "upload_path": "/v1/statements",
  "webhook_path": "/callbacks/3KWPrLbezXsS8uP4YCxyxiQdz0U/{binding_id}/statements/{profile_id}"
}

GET /v1/psps/{id}/statement-profiles

Statement profiles

The PSP's active statement profiles and their version (PSP-11.1). Requires psp:configure.

Parameters

NameInTypeDescription
id requiredpathstring

Responses

  • 200 {version, profiles}.
  • 404 Not your PSP. not_found

Request

curl -X GET "$KULPAY_URL/v1/psps/{id}/statement-profiles" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "profiles": [
    {
      "accounts": [
        "MZ59000800000000123456789"
      ],
      "acknowledgement": "api",
      "arrival_window": "2h",
      "channel": "api_upload",
      "cutoff": "23:00",
      "direction": "push",
      "frequency": "daily",
      "granularity": "transaction",
      "id": "eod",
      "sample_file_id": "3KWPr5AUvVJpugmIt8dBVHit0KE",
      "security": {
        "checksum": "sha256",
        "mtls": false,
        "pgp": false,
        "ssh_key": false
      },
      "sequence_numbering": false,
      "standard": "camt.053",
      "timezone": "Africa/Maputo"
    }
  ],
  "version": 1
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "PSP not found",
  "error": {
    "code": "not_found",
    "message": "PSP not found"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

PUT /v1/psps/{id}/statement-profiles

Declare statement profiles

A new version of the PSP's statement profiles (PSP-11.1): each statement it produces, with its standard, delivery (direction and channel), frequency and cut-off in its time zone, arrival window, granularity, security, file-name pattern, encoding, decimal and date formats, sequence numbering, field mapping, acknowledgement and the accounts it covers. A PSP may declare several (e.g. an intraday API feed and an end-of-day file). Each profile cites a sample from the preview that parsed; without one it cannot be activated. Versioned and audited (config.statement_profile.version_created, config.statement_profile.activated). Requires psp:configure.

Parameters

NameInTypeDescription
id requiredpathstring

Request body application/json required

FieldTypeDescription
profiles requiredobject[]
profiles[].accounts requiredstring[]
profiles[].acknowledgementstring
One of: none, file, api
profiles[].arrival_window requiredstring
profiles[].channel requiredstring
One of: sftp_kulpay, sftp_psp, api_upload, api_polling, webhook
profiles[].currencystring
profiles[].cutoff requiredstring
profiles[].date_formatstring
profiles[].decimal_separatorstring
One of: ., ,
profiles[].delimiterstring
profiles[].direction requiredstring
One of: push, pull
profiles[].encodingstring
One of: utf-8, latin-1
profiles[].file_name_patternstring
profiles[].frequency requiredstring
One of: intraday, hourly, daily, weekly, monthly
profiles[].granularity requiredstring
One of: transaction, batch, summary
profiles[].id requiredstring
profiles[].mappingobject CSV, XLSX: canonical field to column header; JSON: to path ("lines" is the array path).
profiles[].sample_file_idstring From the preview; required before the profiles can be activated.
profiles[].securityobject
profiles[].security.checksumstring
One of: sha256, none
profiles[].security.mtlsboolean
profiles[].security.pgpboolean
profiles[].security.ssh_keyboolean
profiles[].sequence_numberingboolean
profiles[].sheetinteger
profiles[].standard requiredstring camt.052, camt.053, camt.054, MT940, MT942, BAI2, CSV, XLSX, JSON or a named plugin.
profiles[].timezone requiredstring
reason requiredstring

Responses

  • 201 Active.
  • 400 A profile is invalid or its sample does not parse; the problems are named. invalid_request

Request

curl -X PUT "$KULPAY_URL/v1/psps/{id}/statement-profiles" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "profiles": [
    {
      "accounts": [
        "MZ59000800000000123456789"
      ],
      "acknowledgement": "api",
      "arrival_window": "2h",
      "channel": "api_upload",
      "cutoff": "23:00",
      "direction": "push",
      "frequency": "daily",
      "granularity": "transaction",
      "id": "eod",
      "sample_file_id": "3KWPr5AUvVJpugmIt8dBVHit0KE",
      "security": {
        "checksum": "sha256"
      },
      "standard": "camt.053",
      "timezone": "Africa/Maputo"
    }
  ],
  "reason": "end-of-day camt.053"
}'
201 Response application/json
{
  "approval": null,
  "version": {
    "activated_at": "2026-10-10T22:15:46.911188374Z",
    "created_at": "2026-10-10T22:15:46.909605284Z",
    "created_by": "3KWPrAT32mXDTPScHvhjyVEWawA",
    "id": "3KWPrAnaCx5mmnq8rkGpK4AocSM",
    "kind": "statement_profile",
    "psp_id": "3KWPr8ZKH8CL3inFQ5xFPoFVIct",
    "reason": "end-of-day camt.053",
    "status": "active",
    "version": 1
  }
}
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "statement profiles: profiles[0]: upload a sample first (POST /v1/psps/{id}/statement-profiles/preview); it must parse before activation; profiles[1]: the sample does not parse: line 4: amount \"1.250,00\" is not a decimal",
  "error": {
    "code": "invalid_request",
    "message": "statement profiles: profiles[0]: upload a sample first (POST /v1/psps/{id}/statement-profiles/preview); it must parse before activation; profiles[1]: the sample does not parse: line 4: amount \"1.250,00\" is not a decimal"
  },
  "status": 400,
  "title": "Bad Request",
  "type": "about:blank"
}

POST /v1/psps/{id}/statement-profiles/preview

Preview a sample statement

Parses a sample file with a profile and returns the canonical lines (line ID, dates, amount in minor units, direction, references, counterparty, depositor, fee, raw line) before the profile is saved. A sample that parses is kept and its sample_file_id goes in the profile. JSON {profile, sample (base64), filename} or multipart (profile field, sample file). Requires psp:configure.

Parameters

NameInTypeDescription
id requiredpathstring

Request body application/json required

FieldTypeDescription
filenamestring
profile requiredobject
profile.accounts requiredstring[]
profile.acknowledgementstring
One of: none, file, api
profile.arrival_window requiredstring
profile.channel requiredstring
One of: sftp_kulpay, sftp_psp, api_upload, api_polling, webhook
profile.currencystring
profile.cutoff requiredstring
profile.date_formatstring
profile.decimal_separatorstring
One of: ., ,
profile.delimiterstring
profile.direction requiredstring
One of: push, pull
profile.encodingstring
One of: utf-8, latin-1
profile.file_name_patternstring
profile.frequency requiredstring
One of: intraday, hourly, daily, weekly, monthly
profile.granularity requiredstring
One of: transaction, batch, summary
profile.id requiredstring
profile.mappingobject CSV, XLSX: canonical field to column header; JSON: to path ("lines" is the array path).
profile.sample_file_idstring From the preview; required before the profiles can be activated.
profile.securityobject
profile.security.checksumstring
One of: sha256, none
profile.security.mtlsboolean
profile.security.pgpboolean
profile.security.ssh_keyboolean
profile.sequence_numberingboolean
profile.sheetinteger
profile.standard requiredstring camt.052, camt.053, camt.054, MT940, MT942, BAI2, CSV, XLSX, JSON or a named plugin.
profile.timezone requiredstring
sample requiredstring (byte)

Responses

  • 200 {sample_file_id, statement}.
  • 422 The profile is invalid or the sample does not parse (sample_rejected).

Request

curl -X POST "$KULPAY_URL/v1/psps/{id}/statement-profiles/preview" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "profile": {
    "accounts": [
      "MZ59000800000000123456789"
    ],
    "acknowledgement": "none",
    "arrival_window": "2h",
    "channel": "api_upload",
    "cutoff": "23:00",
    "direction": "push",
    "frequency": "daily",
    "granularity": "transaction",
    "id": "eod",
    "security": {
      "checksum": "none"
    },
    "standard": "camt.053",
    "timezone": "Africa/Maputo"
  },
  "sample": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPERvY3VtZW50IHhtbG5zPSJ1cm46aXNvOnN0ZDppc286MjAwMjI6dGVjaDp4c2Q6Y2Ft…"
}'
200 Response application/json
{
  "sample_file_id": "3KWPr5AUvVJpugmIt8dBVHit0KE",
  "statement": {
    "account": "MZ59000800000000123456789",
    "checked_balance": true,
    "closing_minor": 1425050,
    "created_at": "2026-10-08T23:10:00Z",
    "credits_minor": 500050,
    "currency": "MZN",
    "debits_minor": 75000,
    "has_balances": true,
    "lines": [
      {
        "account": "MZ59000800000000123456789",
        "amount_minor": 500050,
        "booking_date": "2026-10-08T00:00:00Z",
        "counterparty": "Loja Exemplo Lda",
        "currency": "MZN",
        "depositor_ref": "KP-ACC-1001",
        "direction": "credit",
        "end_to_end_id": "3KT1BI90QLq2PDUscP8QqzCEW4f",
        "kulpay_reference": "3KT1BI90QLq2PDUscP8QqzCEW4f",
        "line_id": "E-0001",
        "line_no": 1,
        "psp_reference": "BCI8812001",
        "raw": "<NtryRef>E-0001</NtryRef> <Amt Ccy=\"MZN\">5000.50</Amt> <CdtDbtInd>CRDT</CdtDbtInd> <Sts>BOOK</Sts> <BookgDt><Dt>2026-10-…",
        "statement_id": "BCI-20261008-0001",
        "value_date": "2026-10-08T00:00:00Z"
      },
      {
        "account": "MZ59000800000000123456789",
        "amount_minor": 75000,
        "booking_date": "2026-10-08T00:00:00Z",
        "counterparty": "Fornecedor SA",
        "currency": "MZN",
        "direction": "debit",
        "fee_minor": 1500,
        "line_id": "E-0002",
        "line_no": 2,
        "psp_reference": "BCI8812002",
        "raw": "<NtryRef>E-0002</NtryRef> <Amt Ccy=\"MZN\">750.00</Amt> <CdtDbtInd>DBIT</CdtDbtInd> <Sts>BOOK</Sts> <BookgDt><Dt>2026-10-0…",
        "statement_id": "BCI-20261008-0001",
        "value_date": "2026-10-08T00:00:00Z"
      }
    ],
    "opening_minor": 1000000,
    "sequence": "281",
    "standard": "camt.053",
    "statement_id": "BCI-20261008-0001"
  }
}
422 Error · sample_rejected application/problem+json
{
  "code": "sample_rejected",
  "detail": "the sample does not parse: camt.053: XML syntax error on line 1: unexpected EOF",
  "error": {
    "code": "sample_rejected",
    "message": "the sample does not parse: camt.053: XML syntax error on line 1: unexpected EOF"
  },
  "status": 422,
  "title": "Unprocessable Content",
  "type": "about:blank"
}