KulPayAPI Live

API reference

Payments

Read-only payment snapshots.

GET /payments/{provider_payment_id}

Read a payment snapshot

Returns the current state of a payment. Use this to poll when the original charge returned next_action.type=poll, or to reconcile after a missed webhook.

Parameters

NameInTypeDescription
provider_payment_id requiredpathstring Gateway-side payment identifier (the `provider_payment_id` returned in the original `ChargeResult`).

Responses

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

Request

curl -X GET "$KULPAY_URL/payments/{provider_payment_id}" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "authorized_amount": "1250.00",
  "captured_amount": "1250.00",
  "checkout_url": "https://merchant.example.co.mz/kulpay/return",
  "failure_code": "EDM",
  "failure_message": "Pagamento de energia, outubro",
  "last_updated_at": "2026-10-10T09:30:00Z",
  "money": {
    "amount": "100.00",
    "currency": "MZN"
  },
  "next_action": {
    "auth_expires_at": "2026-10-10T09:30:00Z",
    "authorization_code": "EDM",
    "bnpl_provider": "mpesa",
    "entity": "string",
    "expires_at": "2026-10-10T09:30:00Z",
    "masked_dest": "string",
    "otp_hint": "string",
    "otp_length": 1,
    "poll_interval_secs": 1,
    "qr_code_data": "string",
    "qr_code_url": "https://merchant.example.co.mz/kulpay/return",
    "redirect_url": "https://merchant.example.co.mz/kulpay/return",
    "reference": "2003014567",
    "return_url": "https://merchant.example.co.mz/kulpay/return",
    "type": "redirect_to_url"
  },
  "payment_id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
  "provider_payment_id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
  "source": {
    "mobile_wallet": {
      "network": "mpesa",
      "phone_number": "+258840000001"
    },
    "type": "mobile_wallet"
  },
  "status": "pending"
}
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/cash-deposits/intents

Your deposit intents

Newest first, optionally ?status=initiated|reported|expired. Requires transactions:read.

Parameters

NameInTypeDescription
statusquerystring

Responses

  • 200 {data, total}.

Request

curl -X GET "$KULPAY_URL/v1/cash-deposits/intents" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "data": [
    {
      "amount_minor": 125000,
      "beneficiary_metadata": {},
      "created_at": "2026-10-11T10:50:00Z",
      "currency": "MZN",
      "deposit_id": "3KVioCC5V4yauYhsd56OlrZgzWl",
      "depositor_metadata": {
        "name": "Ana Macuácua",
        "phone": "+258841234567"
      },
      "expires_at": "2026-10-12T10:50:00Z",
      "id": "3KCZf1Z3amItUf8AcdAyGBLH0Pu",
      "lookup_id": "3K74gnbXRGdhjvd7xwAFB8mAGoF",
      "mode": "live",
      "partner_reference": "AG-MPT-000981",
      "psp_id": "3KbHx5Qp7eYLffSWnjTF10m1LdZ",
      "reference": "800123456",
      "status": "reported",
      "updated_at": "2026-10-11T10:58:31Z"
    },
    {
      "amount_minor": 450000,
      "beneficiary_metadata": {},
      "created_at": "2026-10-11T11:05:00Z",
      "currency": "MZN",
      "depositor_metadata": {},
      "expires_at": "2026-10-12T11:05:00Z",
      "id": "3Kt4lT89R6ASddnqPMzC0MLmKOI",
      "lookup_id": "3Kp8uNxsD8YF5tP5OXNHBKaZf8M",
      "mode": "live",
      "psp_id": "3KbHx5Qp7eYLffSWnjTF10m1LdZ",
      "reference": "800987654",
      "status": "initiated",
      "updated_at": "2026-10-11T11:05:00Z"
    }
  ],
  "total": 2
}

POST /v1/cash-deposits/intents

Initiate a cash deposit from a reference lookup

Step 2 of a partner cash deposit (PSP-7.8). After POST /v1/name-lookups with method: "cash_deposit" and a reference, register the deposit against the returned lookup_id before taking the cash: the beneficiary, amount and currency are fixed, and the amount must respect the lookup's constraints. One lookup initiates one deposit (a repeat returns it); a lookup expires after at most 15 minutes, an intent after 24 hours if not confirmed (cash_in.initiation_expired).

depositor_metadata and beneficiary_metadata carry whatever the partner needs (string pairs, at most 50 keys, keys up to 40 and values up to 500 characters), returned on the intent and never put in an event (only their keys are). A depositor_metadata.email is told when the deposit is credited. Records cash_in.initiated. Requires transactions:write and an Idempotency-Key.

Parameters

NameInTypeDescription
Idempotency-Keyheaderstring (uuid) Optional client-supplied UUID. The gateway caches the response for this key for the process lifetime; replays return the cached response.

Request body application/json required

FieldTypeDescription
amount requiredstring
beneficiary_metadataobject
currency requiredstring
depositor_metadataobject
lookup_id requiredstring
partner_referencestring The partner's own reference for the deposit.

Responses

  • 201 The intent (status initiated).
  • 403 not_a_partner.
  • 422 deposit_refused: unknown or expired lookup, currency or amount outside the reference's limits, invalid metadata. pilot_limit while the account is a PSP in pilot: the live transaction breaks the pilot setting named in rule (per_transaction, daily, monthly, currency, settings_missing).

Request

curl -X POST "$KULPAY_URL/v1/cash-deposits/intents" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "amount": "1500.00",
  "beneficiary_metadata": {
    "key": "1250.00"
  },
  "currency": "MZN",
  "depositor_metadata": {
    "key": "1250.00"
  },
  "lookup_id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
  "partner_reference": "2003014567"
}'
201 Response application/json
{
  "amount_minor": 125000,
  "beneficiary_metadata": {
    "invoice": "FT2026/10/0042"
  },
  "created_at": "2026-10-11T10:50:00Z",
  "currency": "MZN",
  "depositor_metadata": {
    "email": "ana.macuacua@example.co.mz",
    "name": "Ana Macuácua",
    "phone": "+258841234567"
  },
  "expires_at": "2026-10-12T10:50:00Z",
  "id": "3KCZf1Z3amItUf8AcdAyGBLH0Pu",
  "lookup_id": "3K74gnbXRGdhjvd7xwAFB8mAGoF",
  "mode": "live",
  "partner_reference": "AG-MPT-000981",
  "psp_id": "3KbHx5Qp7eYLffSWnjTF10m1LdZ",
  "reference": "800123456",
  "status": "initiated",
  "updated_at": "2026-10-11T10:50:00Z"
}
403 Error · not_a_partner application/problem+json
{
  "code": "not_a_partner",
  "detail": "this key's account is not a partner bank or agent",
  "error": {
    "code": "not_a_partner",
    "message": "this key's account is not a partner bank or agent"
  },
  "status": 403,
  "title": "Forbidden",
  "type": "about:blank"
}
422 Error · deposit_refused application/problem+json
{
  "code": "deposit_refused",
  "detail": "the lookup has expired: look the reference up again",
  "error": {
    "code": "deposit_refused",
    "message": "the lookup has expired: look the reference up again"
  },
  "status": 422,
  "title": "Unprocessable Content",
  "type": "about:blank"
}

GET /v1/cash-deposits/intents/{id}

A deposit intent

Requires transactions:read.

Parameters

NameInTypeDescription
id requiredpathstring

Responses

  • 200 The intent.
  • 404 Not yours or unknown. not_found

Request

curl -X GET "$KULPAY_URL/v1/cash-deposits/intents/{id}" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "amount_minor": 125000,
  "beneficiary_metadata": {
    "invoice": "FT2026/10/0042"
  },
  "created_at": "2026-10-11T10:50:00Z",
  "currency": "MZN",
  "depositor_metadata": {
    "name": "Ana Macuácua",
    "phone": "+258841234567"
  },
  "expires_at": "2026-10-12T10:50:00Z",
  "id": "3KCZf1Z3amItUf8AcdAyGBLH0Pu",
  "lookup_id": "3K74gnbXRGdhjvd7xwAFB8mAGoF",
  "mode": "live",
  "partner_reference": "AG-MPT-000981",
  "psp_id": "3KbHx5Qp7eYLffSWnjTF10m1LdZ",
  "reference": "800123456",
  "status": "initiated",
  "updated_at": "2026-10-11T10:50:00Z"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such deposit intent",
  "error": {
    "code": "not_found",
    "message": "no such deposit intent"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

POST /v1/cash-deposits/intents/{id}/confirm

Confirm an initiated cash deposit

Step 3 (PSP-7.9): the receiving bank account, the statement entry_id and value_date. The deposit is reported (PSP-7.6) for the intent's amount, currency and beneficiary, and credited only when the receiving account's statement shows the same entry and amount; an unknown beneficiary goes to suspense. Confirming again with the same entry returns the same deposit. Records cash_in.initiation_confirmed and cash_in.reported. Requires transactions:write and an Idempotency-Key. What credits the beneficiary is the PSP's choice (its CASH_IN capability row's credit_on): this confirmation, by default, the statement then reconciling it (cash_in.reconciled, or cash_in.exception for an operator), or the statement line. A credit to a wallet is applied at its resolver at once; the deposit is final for the partner only when its outcome is terminal (credited, returned, failed): a rejected credit goes to suspense and waits for an operator. cash_in.completed tells the partner.

Parameters

NameInTypeDescription
id requiredpathstring
Idempotency-Keyheaderstring (uuid) Optional client-supplied UUID. The gateway caches the response for this key for the process lifetime; replays return the cached response.

Request body application/json required

FieldTypeDescription
account requiredstring
entry_id requiredstring
value_date requiredstring (date)

Responses

  • 200 {intent, deposit}.
  • 404 Not yours or unknown. not_found
  • 422 deposit_refused: expired, or already confirmed with another entry.

Request

curl -X POST "$KULPAY_URL/v1/cash-deposits/intents/{id}/confirm" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "account": "2003014567",
  "entry_id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
  "value_date": "2026-10-31"
}'
200 Response application/json
{
  "deposit": {
    "account": "000800001234567890157",
    "amount_minor": 125000,
    "currency": "MZN",
    "deadline_at": "2026-10-13T10:58:31Z",
    "depositor_ref": "3Kg6UFQm6mLoKocUxwifotF73Ob",
    "entry_id": "BCI-20261011-000731",
    "id": "3KVioCC5V4yauYhsd56OlrZgzWl",
    "mode": "live",
    "psp_id": "3KbHx5Qp7eYLffSWnjTF10m1LdZ",
    "reported_at": "2026-10-11T10:58:31Z",
    "status": "reported",
    "tenant_id": "3K12JLy9OnIwuN9yzoIgIKQpHJX",
    "updated_at": "2026-10-11T10:58:31Z",
    "value_date": "2026-10-11T00:00:00Z"
  },
  "intent": {
    "amount_minor": 125000,
    "beneficiary_metadata": {
      "invoice": "FT2026/10/0042"
    },
    "created_at": "2026-10-11T10:50:00Z",
    "currency": "MZN",
    "deposit_id": "3KVioCC5V4yauYhsd56OlrZgzWl",
    "depositor_metadata": {
      "name": "Ana Macuácua",
      "phone": "+258841234567"
    },
    "expires_at": "2026-10-12T10:50:00Z",
    "id": "3KCZf1Z3amItUf8AcdAyGBLH0Pu",
    "lookup_id": "3K74gnbXRGdhjvd7xwAFB8mAGoF",
    "mode": "live",
    "partner_reference": "AG-MPT-000981",
    "psp_id": "3KbHx5Qp7eYLffSWnjTF10m1LdZ",
    "reference": "800123456",
    "status": "reported",
    "updated_at": "2026-10-11T10:58:31Z"
  }
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such deposit intent",
  "error": {
    "code": "not_found",
    "message": "no such deposit intent"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}
422 Error · deposit_refused application/problem+json
{
  "code": "deposit_refused",
  "detail": "this deposit intent has expired",
  "error": {
    "code": "deposit_refused",
    "message": "this deposit intent has expired"
  },
  "status": 422,
  "title": "Unprocessable Content",
  "type": "about:blank"
}

GET /v1/psp/transactions

Find a transaction by your reference

Parameters

NameInTypeDescription
originator_reference requiredquerystring

Responses

  • 200 {data, total}: only transactions you submitted.

Request

curl -X GET "$KULPAY_URL/v1/psp/transactions" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "data": [
    {
      "amount": "150",
      "created_at": "2026-10-10T22:15:41.050729989Z",
      "currency": "MZN",
      "final": true,
      "id": "3KWPqSicHngSU8NiBPZ0jpFF1P7",
      "method": "mpesa",
      "originator_reference": "ORIG-1",
      "status": "succeeded",
      "type": "sale"
    }
  ],
  "total": 1
}

POST /v1/psp/transactions

Submit a transaction as an originator PSP

One canonical request for every type an originating PSP sends (PSP-7.2): sale charges the source (payments pipeline), payout pays the destination from the available balance, refund refunds one of your own transactions (original_transaction_id). cash_in registers a cash deposit against a reference lookup (lookup_id from POST /v1/name-lookups, method: "cash_deposit"): it is the same engine as POST /v1/cash-deposits/intents (F2-7) — the same validation, pilot limits, bank confirmation (POST /v1/cash-deposits/intents/{id}/confirm) and cash_in.* events — and answers 202 with the intent (intent_id, lookup_id, expires_at); originator_reference becomes its partner_reference. One lookup is one deposit, from either endpoint, and the same Idempotency-Key on the second endpoint replays the first answer. A refused deposit is 422 deposit_refused. A type that completes at once answers 200 with its final result; one still running answers 202 with the KulPay ID (and a Location), and its outcome arrives on your webhooks (payment.*, payout.*, refund.*, cash_in.*). originator_reference is yours, unique per account and mode: the same reference under another Idempotency-Key is 409 duplicate_originator_reference. Records transaction.created (direction inbound, pattern inbound_request). Requires transactions:write plus the type's own scope: payments:write, payouts:write or payments:refund (a cash_in needs none more). Authentication is your secret key; OAuth client credentials and mTLS follow with PSP-8.7.

Parameters

NameInTypeDescription
Idempotency-Keyheaderstring (uuid) Optional client-supplied UUID. The gateway caches the response for this key for the process lifetime; replays return the cached response.

Request body application/json required

FieldTypeDescription
amountstring Decimal string. A refund without one refunds what is left.
beneficiary_metadataobject A cash_in's beneficiary details, as for POST /v1/cash-deposits/intents.
currencystring
depositor_metadataobject A cash_in's depositor details, as for POST /v1/cash-deposits/intents.
descriptionstring
destinationobject A payout's beneficiary.
destination.accountstring
destination.msisdnstring
destination.namestring
lookup_idstring A cash_in's reference lookup (POST /v1/name-lookups).
metadataobject
methodstring mpesa, emola, card, ...
original_transaction_idstring A refund's original sale.
originator_reference requiredstring
sourceobject A sale's payer.
source.card_tokenstring The acquirer's card token; a card number is refused with 400 `raw_card_data_not_accepted`.
source.msisdnstring
type requiredstring
One of: sale, payout, refund, cash_in

Responses

  • 200 Final result.
  • 202 Accepted and still running; the outcome follows on your webhooks.
  • 400 idempotency_key_required, invalid_request or an invalid amount.
  • 403 insufficient_scope with missing_scope.
  • 404 A refund of a transaction that is not yours. not_found
  • 409 duplicate_originator_reference or idempotency_key_reused.
  • 422 not_supported, limit_exceeded, risk_declined, insufficient_balance or not_refundable. pilot_limit while the account is a PSP in pilot: the live transaction breaks the pilot setting named in rule (per_transaction, daily, monthly, currency, settings_missing, or merchant_not_allowed for a sale whose metadata.merchant_id is missing or not on the allow-list).

Request

curl -X POST "$KULPAY_URL/v1/psp/transactions" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "amount": "150.00",
  "currency": "MZN",
  "method": "mpesa",
  "originator_reference": "ORIG-1",
  "source": {
    "msisdn": "258841234567"
  },
  "type": "sale"
}'
200 Response application/json
{
  "amount": "50",
  "created_at": "2026-10-10T22:15:41.467535573Z",
  "currency": "MZN",
  "final": true,
  "id": "3KWPqX2u4Kj2nLVEdBxmgvAdQC6",
  "method": "mpesa",
  "original_transaction_id": "3KWPqSicHngSU8NiBPZ0jpFF1P7",
  "originator_reference": "ORIG-RF",
  "status": "succeeded",
  "type": "refund"
}
202 Response application/json
{
  "amount": "20",
  "created_at": "2026-10-10T22:15:41.267450961Z",
  "currency": "MZN",
  "final": false,
  "id": "3KWPqUxudtg7PGC2cVdwKXcBhpw",
  "method": "mpesa",
  "originator_reference": "ORIG-2",
  "status": "pending",
  "type": "sale"
}
400 Error · idempotency_key_required application/problem+json
{
  "code": "idempotency_key_required",
  "detail": "This request moves money: send an Idempotency-Key header so a retry can never run it twice.",
  "error": {
    "code": "idempotency_key_required",
    "message": "This request moves money: send an Idempotency-Key header so a retry can never run it twice."
  },
  "status": 400,
  "title": "Bad Request",
  "type": "about:blank"
}
403 Error · insufficient_scope application/problem+json
{
  "code": "insufficient_scope",
  "detail": "missing required scope: payments:write",
  "error": {
    "code": "insufficient_scope",
    "message": "missing required scope: payments:write",
    "missing_scope": "payments:write"
  },
  "missing_scope": "payments:write",
  "status": 403,
  "title": "Forbidden",
  "type": "about:blank"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "transaction not found",
  "error": {
    "code": "not_found",
    "message": "transaction not found"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}
409 Error · duplicate_originator_reference application/problem+json
{
  "code": "duplicate_originator_reference",
  "detail": "A transaction with this originator_reference already exists; fetch it with GET /v1/psp/transactions?originator_reference=.",
  "error": {
    "code": "duplicate_originator_reference",
    "message": "A transaction with this originator_reference already exists; fetch it with GET /v1/psp/transactions?originator_reference=."
  },
  "status": 409,
  "title": "Conflict",
  "type": "about:blank"
}
422 Error · not_supported application/problem+json
{
  "code": "not_supported",
  "detail": "This transaction type is not available yet.",
  "error": {
    "code": "not_supported",
    "message": "This transaction type is not available yet."
  },
  "status": 422,
  "title": "Not supported",
  "type": "about:blank"
}

GET /v1/psp/transactions/{id}

A transaction you submitted

Parameters

NameInTypeDescription
id requiredpathstring The KulPay ID.

Responses

  • 200 The transaction.
  • 404 Not found, or not yours. not_found

Request

curl -X GET "$KULPAY_URL/v1/psp/transactions/{id}" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "amount": "150",
  "created_at": "2026-10-10T22:15:41.050729989Z",
  "currency": "MZN",
  "final": true,
  "id": "3KWPqSicHngSU8NiBPZ0jpFF1P7",
  "method": "mpesa",
  "originator_reference": "ORIG-1",
  "status": "succeeded",
  "type": "sale"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "transaction not found",
  "error": {
    "code": "not_found",
    "message": "transaction not found"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

GET /v1/transactions/{id}/reversals

A transaction's reversals

Parameters

NameInTypeDescription
id requiredpathstring Payment id.

Responses

  • 200 {data, total}.

Request

curl -X GET "$KULPAY_URL/v1/transactions/{id}/reversals" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "data": null,
  "total": 0
}

POST /v1/transactions/{id}/reversals

Reverse a transaction

Reverses a duplicated or uncertain transaction through the PSP's own reversal operation (M-Pesa reversal) with the original reference (PSP-6.8). A settled payment: the amount (default: what is left) is reserved, the ledger posts a reversing entry and the payment ends refunded or partially_refunded. A payment still in flight (its outcome uncertain) is reversed whole and ends cancelled. Started here by your own key it runs at once; started by an operator it waits for a second person. A provider without reversals answers 422 not_supported: refund instead. Events: reversal.requested, reversal.succeeded, reversal.failed (the last two are webhooks). Requires transactions:write.

Parameters

NameInTypeDescription
id requiredpathstring Payment id.

Request body application/json required

FieldTypeDescription
amountstring
reason requiredstring

Responses

  • 201 The reversal (a refund of kind reversal).
  • 404 No such payment for this account. not_found
  • 409 The payment cannot be reversed in its state. invalid_state
  • 422 not_supported, or the amount is more than what is left.

Request

curl -X POST "$KULPAY_URL/v1/transactions/{id}/reversals" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "amount": "1250.00",
  "reason": "Pagamento de energia, outubro"
}'
201 Response application/json
{
  "amount": "1250.75",
  "amount_minor": 125075,
  "created_at": "2026-10-11T11:04:52Z",
  "currency": "MZN",
  "deleted_at": null,
  "id": "3KUYZ1VKE7LXO4YAPRrEI8GCcM8",
  "kind": "reversal",
  "mode": "live",
  "payment_id": "3KUYH1DSm7tFw4GIxZzMq8OkBu0",
  "reason": "Duplicate charge: the customer paid the same order twice",
  "refund_ref": "MPS-REV-88123456",
  "status": "succeeded",
  "updated_at": "2026-10-11T11:04:55Z"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "transaction not found",
  "error": {
    "code": "not_found",
    "message": "transaction not found"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}
409 Error · invalid_state application/problem+json
{
  "code": "invalid_state",
  "detail": "payments: the payment cannot be reversed in its state",
  "error": {
    "code": "invalid_state",
    "message": "payments: the payment cannot be reversed in its state"
  },
  "status": 409,
  "title": "Not reversible",
  "type": "about:blank"
}
422 Error · not_supported application/problem+json
{
  "code": "not_supported",
  "detail": "the payment's provider does not support reversals",
  "error": {
    "code": "not_supported",
    "message": "the payment's provider does not support reversals",
    "rule": "reversal"
  },
  "rule": "reversal",
  "status": 422,
  "title": "Not supported",
  "type": "about:blank"
}