API 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. Verification admits a callback; only a driver that authenticates its own callbacks (Stripe's signature, kulpay_balance's read-back) is applied as sent. M-Pesa and e-Mola callbacks are wake-ups: the payment moves only on the provider's answer to a status query (IG-82). 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.
  • 404 No such endpoint (unknown binding, or not this PSP's). gateway.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 · gateway.callback_rejected application/problem+json
{
  "code": "callback_rejected",
  "detail": "The callback did not pass this binding's verification.",
  "error": {
    "code": "gateway.callback_rejected",
    "message": "The callback did not pass this binding's verification."
  },
  "legacy_code": "gateway.callback_rejected",
  "status": 401,
  "title": "Callback rejected",
  "type": "about:blank"
}
404 Error · gateway.not_found application/problem+json
{
  "code": "not_found",
  "detail": "No such callback endpoint.",
  "error": {
    "code": "gateway.not_found",
    "message": "No such callback endpoint."
  },
  "legacy_code": "gateway.not_found",
  "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/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 and void flows, then idempotency replay, webhook delivery, redelivery and a badly signed webhook to the PSP's endpoint, and the sample statement (CSV) matched against the run's payments. With rerun_of, only that run's failed cases run again; the passed ones are carried over; with only_case as well, only that one case. When every mandatory case passes, a report is signed (audit signing key). With no case failed the PSP is certified and, from technical_setup, moves to certification. With optional cases failed the run is passed but does not certify: it records psp.certification.exceptions, and the PSP is certified once an operator prepares the acceptance of those cases (approval psp.certification.accept) and a second one approves it, or after a run where they pass. Records psp.certification.run_started, case_failed, passed or failed. Requires accounts:write.

Parameters

NameInTypeDescription
id requiredpathstring

Request body application/json

FieldTypeDescription
only_casestring With rerun_of: repeat only this failed case (its key); every other case is carried over as it was.
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}/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, routing, retry_policy 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.

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

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

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

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

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}/go-live

Pilot and go-live state

Pilot gates (KYB approved, contract signed, commercial terms approved, and for a PSP certification passed), live gates (7 days in pilot since entry or the last critical incident resolved, no open critical incident, and for a PSP live statements since pilot all matched, for an LSP a live remittance paid to its biller), each with detail and detail_pt (Portuguese). Once the live gates are met the gateway prepares the go-live request by itself; a second person approves it, 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"
}

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
}

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

A partner's requirements

Any partner's requirements, as GET /v1/psps/{id}/requirements gives its own. Requires accounts:read.

Parameters

NameInTypeDescription
id requiredpathstring
langquerystring The language of the names and help (pt by default).
One of: pt, en

Responses

  • 200 The requirements.
  • 404 The named resource was not found. not_found

Request

curl -X GET "$KULPAY_URL/v1/admin/psps/{id}/requirements" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "blocked": [],
  "currently_due": [
    {
      "blocks": "pilot",
      "due_by": "2026-11-04T09:00:00Z",
      "key": "webhooks",
      "name": "Webhooks validated",
      "on_past_due": "warn",
      "required": true,
      "stage": "technical",
      "status": "currently_due",
      "type": "configuration",
      "who": "partner"
    }
  ],
  "done": [],
  "errors": [],
  "eventually_due": [],
  "optional": [],
  "past_due": [],
  "pending_verification": [],
  "psp_id": "3KWPlauKgv7rrVi1Wsue46ebEDF",
  "stage": "technical"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "No payment with this id.",
  "error": {
    "code": "not_found",
    "message": "No payment with this id."
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

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. An Idempotency-Key on this request binds the key to it as usual, but the success response holds a secret and is never kept (IG-30): the same key and request again is 409 idempotency_replay_unavailable (never the secret a second time; list the resources to find the one it made), another request under the key 409 idempotency_key_reused.

Request body application/json required

FieldTypeDescription
biller_categorystring For an LSP: its sector, a code of the biller_sectors catalogue.
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
kindstring psp (a payment provider) or lsp (a biller).
One of: psp, lsp
languagestring
One of: pt, en
legal_namestring
licencesobject[]
licences[].expires_onstring (date)
licences[].issuerstring
licences[].numberstring
licences[].typestring
partner_typestring The partner type, a code of the partner_types catalogue (GET /v1/catalogs/partner_types?kind=psp). It sets the template the technical setup starts from.
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).
  • 409 idempotency_replay_unavailable: a replay of a request that succeeded; idempotency_key_reused: another request under the key.

Request

curl -X POST "$KULPAY_URL/v1/psps/applications" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "compliance_contact": {
    "email": "opsexample.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": "opsexample.co.mz",
    "name": "Ana"
  },
  "transaction_types": [
    "mobile_money"
  ]
}'
201 Response application/json
{
  "data": {
    "compliance_contact": {
      "email": "opsexample.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": "opsexample.co.mz",
      "name": "Ana"
    },
    "transaction_types": [
      "mobile_money"
    ]
  },
  "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"
}
409 Error · idempotency_replay_unavailable application/problem+json
{
  "code": "idempotency_replay_unavailable",
  "detail": "A request with this Idempotency-Key already succeeded. Its response held a secret that is shown once and not kept, so it cannot be replayed: list the resources to find the one it created, and revoke it if its secret was lost.",
  "error": {
    "code": "idempotency_replay_unavailable",
    "message": "A request with this Idempotency-Key already succeeded. Its response held a secret that is shown once and not kept, so it cannot be replayed: list the resources to find the one it created, and revoke it if its secret was lost."
  },
  "status": 409,
  "title": "Conflict",
  "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": "opsexample.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": "opsexample.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
biller_categorystring For an LSP: its sector, a code of the biller_sectors catalogue.
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
kindstring psp (a payment provider) or lsp (a biller).
One of: psp, lsp
languagestring
One of: pt, en
legal_namestring
licencesobject[]
licences[].expires_onstring (date)
licences[].issuerstring
licences[].numberstring
licences[].typestring
partner_typestring The partner type, a code of the partner_types catalogue (GET /v1/catalogs/partner_types?kind=psp). It sets the template the technical setup starts from.
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": "opsexample.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": "opsexample.co.mz",
    "name": "Ana"
  },
  "transaction_types": [
    "mobile_money"
  ]
}'
200 Response application/json
{
  "data": {
    "compliance_contact": {
      "email": "opsexample.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": "opsexample.co.mz",
      "name": "Ana"
    },
    "transaction_types": [
      "mobile_money"
    ]
  },
  "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}/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}/requirements

Your requirements

For the partner the key belongs to (scope psp:configure): what is past due, due now, due later, waiting for someone, refused, done and optional, each with its stage, who does it, its deadline and the capability it protects; and the capabilities closed by a past-due requirement set to block. The stages advance by themselves once a stage's required requirements are met. Also in the portal (Requisitos).

Parameters

NameInTypeDescription
id requiredpathstring
langquerystring The language of the names and help (pt by default).
One of: pt, en

Responses

  • 200 The requirements.
  • 404 The named resource was not found. not_found

Request

curl -X GET "$KULPAY_URL/v1/psps/{id}/requirements" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "blocked": [],
  "currently_due": [
    {
      "blocks": "pilot",
      "due_by": "2026-11-04T09:00:00Z",
      "key": "webhooks",
      "name": "Webhooks validated",
      "on_past_due": "warn",
      "required": true,
      "stage": "technical",
      "status": "currently_due",
      "type": "configuration",
      "who": "partner"
    }
  ],
  "done": [],
  "errors": [],
  "eventually_due": [],
  "optional": [],
  "past_due": [],
  "pending_verification": [],
  "psp_id": "3KWPlauKgv7rrVi1Wsue46ebEDF",
  "stage": "technical"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "No payment with this id.",
  "error": {
    "code": "not_found",
    "message": "No payment with this id."
  },
  "status": 404,
  "title": "Not Found",
  "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, h2h
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, h2h
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"
}

GET /v1/service/partners/lsp/{external_ref}

Read a provisioned LSP (trusted service)

The gateway's side of the biller KulOffice provisioned under its id: lifecycle, KYB basis, statuses, sandbox and invitation. 404 when this key's account provisioned none under it. Requires service:partners:write.

Parameters

NameInTypeDescription
external_ref requiredpathstring KulOffice's id for the biller (letters, digits and _ . : -, at most 64).

Responses

  • 200 The provisioned LSP.
  • 401 Missing or invalid bearer token. unauthorized
  • 404 No LSP provisioned under it by this account. not_found

Request

curl -X GET "$KULPAY_URL/v1/service/partners/lsp/{external_ref}" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "partner": {
    "account_id": "3KxWijN5vpLi9ArGysmtkgU8KY0",
    "biller": {
      "code": "AGUAS_UMB",
      "commercial_status": "approved",
      "id": "3KxWiihSQFA9KqUw9XlXiOhMucA",
      "status": "draft",
      "technical_status": "sandbox"
    },
    "created_at": "2026-10-20T12:37:08.9787124Z",
    "external_ref": "bil_3KQ7",
    "invitation": {
      "email": "a•••@umbeluzi.co.mz",
      "status": "sent"
    },
    "kyb": {
      "approved_at": "2026-10-20T09:00:00Z",
      "approved_by": "op_checker",
      "basis": "kuloffice",
      "decision_ref": "apr_91",
      "status": "approved"
    },
    "lifecycle_status": "technical_setup",
    "origin": "kuloffice",
    "psp_id": "3KxWinedj8leqZYlcZUkoxDbGEZ",
    "sandbox": {
      "enabled": true,
      "enabled_at": "2026-10-20T12:37:09.0079515Z"
    },
    "updated_at": "2026-10-20T14:37:09.0223855+02:00"
  }
}
401 Error · unauthorized application/problem+json
{
  "code": "unauthorized",
  "detail": "Missing or invalid API key.",
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  },
  "status": 401,
  "title": "Unauthorized",
  "type": "about:blank"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "No LSP was provisioned under this external_ref by this key's account.",
  "error": {
    "code": "not_found",
    "message": "No LSP was provisioned under this external_ref by this key's account."
  },
  "status": 404,
  "title": "Not found",
  "type": "about:blank"
}

PUT /v1/service/partners/lsp/{external_ref}

Provision a biller KulOffice approved, as an LSP in technical setup (trusted service)

KO-1 (owner decision 2026-10-20): KulOffice and KulPay are the same regulated entity, so KulPay relies on KulOffice's KYB and four-eyes approval of a biller and does not repeat it. One idempotent call per biller, by KulOffice's id for it. The first call, with status: approved and the kyb and approval decisions, creates the organisation (account active), its LSP straight in technical_setup with its KYB recorded as KulOffice's (psp.kyb.approved v2, basis: kuloffice), its sandbox, and the biller (origin kuloffice, technical status sandbox, status draft); it makes the technical contact the portal's first admin and emails them a single-use sign-in link valid 72 hours. It answers 201 with created: true. Later calls answer 200: they keep the commercial fields equal to KulOffice's (name, category, NUIT, logo, contacts, credit accounts; the default bank account is the remittance account) and its commercial status (approved, suspended, terminated); a new technical contact is invited, and each address in revoked_contacts (KO-2: a technical contact replaced in KulOffice with a reason and consent) loses its portal access at once: user removed, sessions ended, told by email; a revoked address is never invited back by a sync. A replacement the business makes in the portal is sent to KulOffice as the webhook biller.technical_contact_replaced (phone masked). Those fields are read only at KulPay (409 owned_by_origin on the admin routes). KulPay owns the technical side: sandbox, certification, go-live with four eyes. Every technical status is the webhook biller.integration_status on the key's account (KulOffice's), in both modes. The biller takes payments only when KulOffice has it approved and its LSP is live. Without the portal's address the invitation cannot go out: nothing is created, 503 portal_not_configured with Retry-After. Requires service:partners:write (platform, operator-only). Never certifies or puts an LSP live.

Parameters

NameInTypeDescription
external_ref requiredpathstring KulOffice's id for the biller (letters, digits and _ . : -, at most 64).

Request body application/json required

FieldTypeDescription
approvalobject
approval.at requiredstring (date-time)
approval.by requiredstring KulOffice's operator who decided.
approval.refstring KulOffice's reference for the decision, kept for the audit trail.
category requiredstring
One of: electricity, water, telecom, tv, internet, education, insurance, government, other
code requiredstring The biller's code at KulPay: 2 to 20 capitals, digits or _. It does not change (`409 code_immutable`).
contacts requiredobject[] By role; a technical contact with an email is required (KulPay invites them).
contacts[].email requiredstring
contacts[].namestring
contacts[].phonestring
contacts[].role requiredstring
One of: technical, commercial, finance, compliance
countrystring ISO-3166 alpha-2; MZ when absent.
credit_accountsobject[]
credit_accounts[].credit_account_idstring
credit_accounts[].defaultboolean
credit_accounts[].holderstring
credit_accounts[].institutionstring
credit_accounts[].number requiredstring
credit_accounts[].type requiredstring
One of: bank_account, mobile_wallet, kulpay_balance
kybobject
kyb.at requiredstring (date-time)
kyb.by requiredstring KulOffice's operator who decided.
kyb.refstring KulOffice's reference for the decision, kept for the audit trail.
legal_name requiredstring
logo_urlstring https only.
nuit requiredstring
reasonstring Why the commercial status changed.
revoked_contactsobject[] KO-2: people whose access KulOffice revoked when it replaced them as the technical contact. The Gateway removes their portal user, ends their sessions and tells them; a revoked address is never invited back by a later call, only by an admin's own invitation in the portal.
revoked_contacts[].atstring (date-time)
revoked_contacts[].bystring Who in KulOffice revoked it.
revoked_contacts[].email requiredstring
revoked_contacts[].reasonstring
status requiredstring KulOffice's commercial status. Only `approved` creates.
One of: approved, suspended, terminated
trading_namestring Shown to payers; the legal name when absent.

Responses

  • 200 Already provisioned: its commercial fields updated.
  • 201 Provisioned: the LSP in technical setup, its sandbox opened, its technical contact invited.
  • 400 A field is missing or invalid; detail names it. invalid_request
  • 401 Missing or invalid bearer token. unauthorized
  • 403 The key does not hold service:partners:write. insufficient_scope
  • 409 not_approved: nothing is provisioned before KulOffice approves; code_taken: another biller has the code; code_immutable: the code changed.
  • 429 Per-identity rate limit exceeded. Retry-After indicates when to retry. rate_limited
  • 503 portal_not_configured: the invitation cannot go out; nothing was created; retry after Retry-After.

Request

curl -X PUT "$KULPAY_URL/v1/service/partners/lsp/{external_ref}" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "approval": {
    "at": "2026-10-20T09:00:00Z",
    "by": "op_checker",
    "ref": "apr_91"
  },
  "category": "water",
  "code": "AGUAS_UMB",
  "contacts": [
    {
      "email": "ana.tecnica@umbeluzi.co.mz",
      "name": "Ana Técnica",
      "role": "technical"
    },
    {
      "email": "rui@umbeluzi.co.mz",
      "name": "Rui Comercial",
      "phone": "+258841110000",
      "role": "commercial"
    }
  ],
  "country": "MZ",
  "credit_accounts": [
    {
      "credit_account_id": "bca_1",
      "default": true,
      "holder": "Águas do Umbeluzi",
      "institution": "BCI",
      "number": "000123456789",
      "type": "bank_account"
    }
  ],
  "kyb": {
    "at": "2026-10-20T08:00:00Z",
    "by": "op_maker",
    "ref": "kyb_77"
  },
  "legal_name": "Águas do Umbeluzi, SA",
  "nuit": "400123456",
  "status": "approved",
  "trading_name": "Águas do Umbeluzi"
}'
200 Response application/json
{
  "created": false,
  "partner": {
    "account_id": "3KxWijN5vpLi9ArGysmtkgU8KY0",
    "biller": {
      "code": "AGUAS_UMB",
      "commercial_status": "suspended",
      "id": "3KxWiihSQFA9KqUw9XlXiOhMucA",
      "status": "draft",
      "technical_status": "sandbox"
    },
    "created_at": "2026-10-20T12:37:08.9787124Z",
    "external_ref": "bil_3KQ7",
    "invitation": {
      "email": "a•••@umbeluzi.co.mz",
      "status": "sent"
    },
    "kyb": {
      "approved_at": "2026-10-20T09:00:00Z",
      "approved_by": "op_checker",
      "basis": "kuloffice",
      "decision_ref": "apr_91",
      "status": "approved"
    },
    "lifecycle_status": "technical_setup",
    "origin": "kuloffice",
    "psp_id": "3KxWinedj8leqZYlcZUkoxDbGEZ",
    "sandbox": {
      "enabled": true,
      "enabled_at": "2026-10-20T12:37:09.0079515Z"
    },
    "updated_at": "2026-10-20T14:37:09.0407043+02:00"
  }
}
201 Response application/json
{
  "created": true,
  "partner": {
    "account_id": "3KxWijN5vpLi9ArGysmtkgU8KY0",
    "biller": {
      "code": "AGUAS_UMB",
      "commercial_status": "approved",
      "id": "3KxWiihSQFA9KqUw9XlXiOhMucA",
      "status": "draft",
      "technical_status": "sandbox"
    },
    "created_at": "2026-10-20T12:37:08.9787124Z",
    "external_ref": "bil_3KQ7",
    "invitation": {
      "email": "a•••@umbeluzi.co.mz",
      "status": "sent"
    },
    "kyb": {
      "approved_at": "2026-10-20T09:00:00Z",
      "approved_by": "op_checker",
      "basis": "kuloffice",
      "decision_ref": "apr_91",
      "status": "approved"
    },
    "lifecycle_status": "technical_setup",
    "origin": "kuloffice",
    "psp_id": "3KxWinedj8leqZYlcZUkoxDbGEZ",
    "sandbox": {
      "enabled": true,
      "enabled_at": "2026-10-20T12:37:09.0079515Z"
    },
    "updated_at": "2026-10-20T12:37:08.9787124Z"
  }
}
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "code: 2 to 20 capital letters, digits or _",
  "error": {
    "code": "invalid_request",
    "message": "code: 2 to 20 capital letters, digits or _"
  },
  "status": 400,
  "title": "Bad Request",
  "type": "about:blank"
}
401 Error · unauthorized application/problem+json
{
  "code": "unauthorized",
  "detail": "Missing or invalid API key.",
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  },
  "status": 401,
  "title": "Unauthorized",
  "type": "about:blank"
}
403 Error · insufficient_scope application/problem+json
{
  "code": "insufficient_scope",
  "detail": "missing required scope: service:partners:write",
  "error": {
    "code": "insufficient_scope",
    "message": "missing required scope: service:partners:write"
  },
  "missing_scope": "service:partners:write",
  "status": 403,
  "title": "Forbidden",
  "type": "about:blank"
}
409 Error · not_approved application/problem+json
{
  "code": "not_approved",
  "detail": "KulPay provisions a biller once KulOffice approved it: send status approved, with its kyb and approval decisions.",
  "error": {
    "code": "not_approved",
    "message": "KulPay provisions a biller once KulOffice approved it: send status approved, with its kyb and approval decisions."
  },
  "status": 409,
  "title": "Not approved",
  "type": "about:blank"
}
429 Error · rate_limited application/problem+json
{
  "code": "rate_limited",
  "detail": "Retry after the number of seconds in Retry-After.",
  "error": {
    "code": "rate_limited",
    "message": "Retry after the number of seconds in Retry-After."
  },
  "status": 429,
  "title": "Too Many Requests",
  "type": "about:blank"
}
503 Error · portal_not_configured application/problem+json
{
  "code": "portal_not_configured",
  "detail": "The partner portal's address is not configured at the gateway, so the technical contact cannot be invited. Nothing was created; retry once an operator sets it.",
  "error": {
    "code": "portal_not_configured",
    "message": "The partner portal's address is not configured at the gateway, so the technical contact cannot be invited. Nothing was created; retry once an operator sets it."
  },
  "status": 503,
  "title": "Portal not configured",
  "type": "about:blank"
}

POST /v1/service/partners/lsp/{external_ref}/invitation

Send the technical contact a new invitation (trusted service)

A new single-use sign-in link, valid 72 hours, to the technical contact; at most 5 links an hour to one address (429). The portal of a suspended or offboarded LSP is closed (400). Requires service:partners:write.

Parameters

NameInTypeDescription
external_ref requiredpathstring KulOffice's id for the biller (letters, digits and _ . : -, at most 64).

Responses

  • 202 Sent.
  • 401 Missing or invalid bearer token. unauthorized
  • 404 The named resource was not found. not_found
  • 429 Per-identity rate limit exceeded. Retry-After indicates when to retry. rate_limited

Request

curl -X POST "$KULPAY_URL/v1/service/partners/lsp/{external_ref}/invitation" \
  -H "Authorization: Bearer $KULPAY_KEY"
202 Response application/json
{
  "partner": {
    "account_id": "3KxWijN5vpLi9ArGysmtkgU8KY0",
    "biller": {
      "code": "AGUAS_UMB",
      "commercial_status": "suspended",
      "id": "3KxWiihSQFA9KqUw9XlXiOhMucA",
      "status": "draft",
      "technical_status": "sandbox"
    },
    "created_at": "2026-10-20T12:37:08.9787124Z",
    "external_ref": "bil_3KQ7",
    "invitation": {
      "email": "a•••@umbeluzi.co.mz",
      "status": "sent"
    },
    "kyb": {
      "approved_at": "2026-10-20T09:00:00Z",
      "approved_by": "op_checker",
      "basis": "kuloffice",
      "decision_ref": "apr_91",
      "status": "approved"
    },
    "lifecycle_status": "technical_setup",
    "origin": "kuloffice",
    "psp_id": "3KxWinedj8leqZYlcZUkoxDbGEZ",
    "sandbox": {
      "enabled": true,
      "enabled_at": "2026-10-20T12:37:09.0079515Z"
    },
    "updated_at": "2026-10-20T14:37:09.0407043+02:00"
  }
}
401 Error · unauthorized application/problem+json
{
  "code": "unauthorized",
  "detail": "Missing or invalid API key.",
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  },
  "status": 401,
  "title": "Unauthorized",
  "type": "about:blank"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "No payment with this id.",
  "error": {
    "code": "not_found",
    "message": "No payment with this id."
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}
429 Error · rate_limited application/problem+json
{
  "code": "rate_limited",
  "detail": "Retry after the number of seconds in Retry-After.",
  "error": {
    "code": "rate_limited",
    "message": "Retry after the number of seconds in Retry-After."
  },
  "status": 429,
  "title": "Too Many Requests",
  "type": "about:blank"
}