API reference
Incoming transfers
Transfers a bank or PSP certified for account_funding.incoming reports as received for a KulPay beneficiary (integration plan phase 5B): resolved privately, final only when the partner says so, and held in suspense until an operator resolves it when nobody matches.
GET /v1/admin/incoming-transfers
Every incoming transfer
For operators, by status (e.g. unmatched: the money in suspense). Requires reconciliation:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
status | query | string |
Responses
- 200
{data, total}.
Request
curl -X GET "$KULPAY_URL/v1/admin/incoming-transfers" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"data": [
{
"amount_minor": 125000,
"currency": "MZN",
"destination_type": "alias",
"destination_value": "LOJA-404",
"final": true,
"final_at": "2026-10-11T09:30:00Z",
"id": "3KdW1pZ0incomingTransfer001",
"in_suspense": true,
"mode": "live",
"partner_account_id": "3KbHx5Qp7eYLffSWnjTF10m1Acc",
"partner_reference": "BCI-TRF-20261011-0042",
"psp_id": "3KbHx5Qp7eYLffSWnjTF10m1LdZ",
"reason": "no KulPay account has this alias",
"received_at": "2026-10-11T09:30:00Z",
"status": "unmatched",
"updated_at": "2026-10-11T09:30:01Z",
"version": 2
}
],
"total": 1
}
POST /v1/admin/incoming-transfers/{id}/resolutions
Resolve an incoming transfer
A typed operator action, never an edit. compliance_review (from received, matched, unmatched or confirmed_final) holds the transfer at once: 200 {transfer}. bind (to account_id, from unmatched or compliance review), release (back to its beneficiary, from compliance review) and return (to its source, from unmatched or compliance review) wait for a second person: 202 {approval}, decided with POST /v1/admin/approvals/{id}/decide, and applied only if the transfer has not changed since. A reason is always required. Requires reconciliation:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string |
Request body application/json required
| Field | Type | Description |
|---|---|---|
account_id | string | |
action required | string | One of: bind, release, return, compliance_review |
reason required | string |
Responses
- 200 Held for compliance review:
{transfer}. - 202 Waiting for approval:
{approval}. - 400 Malformed: unknown action, no reason, no such account.
invalid_request - 404 Unknown, or not the caller's (partner or beneficiary) in this mode.
not_found - 409 The transfer's state does not allow the action, or a decision is already waiting.
invalid_state
Request
curl -X POST "$KULPAY_URL/v1/admin/incoming-transfers/{id}/resolutions" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"account_id": "3Ky2WEM0zMfI2pD5i2iBhSEQcB2",
"action": "bind",
"reason": "The customer'\''s deposit tag was mistyped (LOJA-404 for LOJA-04)"
}'
200 Response application/json
{
"transfer": {
"amount_minor": 125000,
"currency": "MZN",
"destination_type": "alias",
"destination_value": "LOJA-404",
"final": true,
"final_at": "2026-10-11T09:30:00Z",
"id": "3KdW1pZ0incomingTransfer001",
"in_suspense": true,
"mode": "live",
"partner_account_id": "3KbHx5Qp7eYLffSWnjTF10m1Acc",
"partner_reference": "BCI-TRF-20261011-0042",
"psp_id": "3KbHx5Qp7eYLffSWnjTF10m1LdZ",
"reason": "Originator on a watch list",
"received_at": "2026-10-11T09:30:00Z",
"status": "compliance_review",
"updated_at": "2026-10-11T09:30:01Z",
"version": 3
}
}
202 Response application/json
{
"approval": {
"created_at": "2026-10-11T10:02:00Z",
"id": "3KbIf49pShcqGqJUCjC2svkIwHr",
"kind": "incoming_transfer.resolve",
"payload": "{\"action\":\"bind\",\"account_id\":\"3Ky2WEM0zMfI2pD5i2iBhSEQcB2\",\"version\":2}",
"prepared_by": "operator:ana",
"reason": "The customer's deposit tag was mistyped (LOJA-404 for LOJA-04)",
"status": "pending",
"subject_id": "3KdW1pZ0incomingTransfer001",
"tenant_id": "3KbHx5Qp7eYLffSWnjTF10m1Acc"
}
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "a reason is required",
"error": {
"code": "invalid_request",
"message": "a reason is required"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such incoming transfer",
"error": {
"code": "not_found",
"message": "no such incoming transfer"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
409 Error · invalid_state application/problem+json
{
"code": "invalid_state",
"detail": "only an unmatched transfer or one in compliance review can be bound or returned",
"error": {
"code": "invalid_state",
"message": "only an unmatched transfer or one in compliance review can be bound or returned"
},
"status": 409,
"title": "Conflict",
"type": "about:blank"
}
GET /v1/incoming-transfers
Find incoming transfers by partner reference
The transfers the key's account reported or receives, in the key's mode, by partner_reference and/or status, newest first. status matches the status the key's account is shown: the reporting partner never finds a transfer under compliance_review, but under the ordinary state it is shown. Requires transactions:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
partner_reference | query | string | |
status | query | string | The status the key's account is shown (see IncomingTransfer.status). |
Responses
- 200
{data, total}.
Request
curl -X GET "$KULPAY_URL/v1/incoming-transfers" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"data": [
{
"amount_minor": 125000,
"beneficiary_name": "L*** D*** L**",
"creditable": false,
"currency": "MZN",
"destination": {
"type": "deposit_reference",
"value": "INV-1001"
},
"final": false,
"id": "3KdW1pZ0incomingTransfer001",
"mode": "live",
"partner_reference": "BCI-TRF-20261011-0042",
"psp_id": "3KbHx5Qp7eYLffSWnjTF10m1LdZ",
"received_at": "2026-10-11T09:30:00Z",
"status": "matched",
"updated_at": "2026-10-11T09:30:01Z",
"value_date": "2026-10-11",
"version": 2
}
],
"total": 1
}
POST /v1/incoming-transfers
Report an incoming transfer
A bank or PSP certified for the account_funding.incoming capability (an approved capability matrix entry for the mode and currency; live needs a partner in pilot or live) reports a transfer it received for a KulPay beneficiary, named by a deposit_reference, an alias (deposit tag) or a virtual_account. Amounts are integer minor units. Any other partner is refused (403 capability_not_enabled, or not_a_partner for a key that is no partner's) before anything is stored.
The transfer is idempotent by your partner_reference in the mode: the same report returns the same transfer (200), the same reference with other facts is 409 partner_reference_conflict. The gateway resolves the beneficiary privately: you see whether it matched and the holder's masked name, never its account. Finality is explicit: send final: true only for money that cannot be recalled, else confirm it later with POST /v1/incoming-transfers/{id}/finality. Only a matched, final transfer is creditable. Unmatched final money stays in suspense until an operator binds it or returns it (you then return the funds). A transfer an operator holds for review is shown to the reporting partner in the ordinary state its facts give (matched, confirmed_final or unmatched), in reads, lists, the status filter, events and webhooks; compliance_review and its event go only to the beneficiary and operators.
Events and webhooks: incoming_transfer.received, incoming_transfer.matched, incoming_transfer.unmatched, incoming_transfer.finality_confirmed, incoming_transfer.confirmed_final, incoming_transfer.compliance_review (beneficiary only), incoming_transfer.returned, incoming_transfer.credited, with identifiers, amount, status, final and version only; read the transfer back through this API. Requires transactions:write. The launch capability matrix (plan phase 10) holds the route first: one it does not enable is 422 route_not_enabled, and anything off_net 422 movement_scope_disabled, before any limit is reserved or provider called.
Request body application/json required
| Field | Type | Description |
|---|---|---|
amount_minor required | integer | |
currency required | string | |
destination required | object | |
destination.type required | string | One of: deposit_reference, alias, virtual_account, payment_reference |
destination.value required | string | |
final | boolean | |
partner_reference required | string | Your own transfer reference. |
value_date | string (date) |
Responses
- 200 The same report was recorded before: that transfer.
- 201 Recorded now;
statusismatched,unmatchedor, when the beneficiary could not be asked yet,received. - 400 The report is malformed.
invalid_request - 403 Not certified for account_funding.incoming (or not a partner:
not_a_partner). Nothing was stored.capability_not_enabled - 409 The reference was reported with other facts.
partner_reference_conflict - 422 The route is not in the launch matrix (route_not_enabled) or would be off_net (movement_scope_disabled).
route_not_enabled
Request
curl -X POST "$KULPAY_URL/v1/incoming-transfers" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount_minor": 125000,
"currency": "MZN",
"destination": {
"type": "deposit_reference",
"value": "INV-1001"
},
"final": false,
"partner_reference": "BCI-TRF-20261011-0042",
"value_date": "2026-10-11"
}'
200 Response application/json
{
"amount_minor": 125000,
"beneficiary_name": "L*** D*** L**",
"creditable": false,
"currency": "MZN",
"destination": {
"type": "deposit_reference",
"value": "INV-1001"
},
"final": false,
"id": "3KdW1pZ0incomingTransfer001",
"mode": "live",
"partner_reference": "BCI-TRF-20261011-0042",
"psp_id": "3KbHx5Qp7eYLffSWnjTF10m1LdZ",
"received_at": "2026-10-11T09:30:00Z",
"status": "matched",
"updated_at": "2026-10-11T09:30:01Z",
"value_date": "2026-10-11",
"version": 2
}
201 Response application/json
{
"amount_minor": 125000,
"beneficiary_name": "L*** D*** L**",
"creditable": false,
"currency": "MZN",
"destination": {
"type": "deposit_reference",
"value": "INV-1001"
},
"final": false,
"id": "3KdW1pZ0incomingTransfer001",
"mode": "live",
"partner_reference": "BCI-TRF-20261011-0042",
"psp_id": "3KbHx5Qp7eYLffSWnjTF10m1LdZ",
"received_at": "2026-10-11T09:30:00Z",
"status": "matched",
"updated_at": "2026-10-11T09:30:01Z",
"value_date": "2026-10-11",
"version": 2
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "destination.type must be deposit_reference, alias or virtual_account",
"error": {
"code": "invalid_request",
"message": "destination.type must be deposit_reference, alias or virtual_account"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
403 Error · capability_not_enabled application/problem+json
{
"code": "capability_not_enabled",
"detail": "account_funding.incoming is not enabled for this partner: not approved in live mode",
"error": {
"code": "capability_not_enabled",
"message": "account_funding.incoming is not enabled for this partner: not approved in live mode"
},
"status": 403,
"title": "Forbidden",
"type": "about:blank"
}
409 Error · partner_reference_conflict application/problem+json
{
"code": "partner_reference_conflict",
"detail": "payments: this partner_reference was reported with different facts",
"error": {
"code": "partner_reference_conflict",
"message": "payments: this partner_reference was reported with different facts"
},
"status": 409,
"title": "Conflict",
"type": "about:blank"
}
422 Error · route_not_enabled application/problem+json
{
"code": "route_not_enabled",
"detail": "the charge route settling/mpesa (live, acc_top) is not enabled in launch matrix 2026-10-12.1",
"rejection_id": "3KfS0route0not0enabled00001",
"rule": "launch_matrix",
"status": 422,
"title": "Route not enabled",
"type": "about:blank"
}
GET /v1/incoming-transfers/{id}
An incoming transfer
For the partner that reported it and, once matched, its beneficiary, in the key's mode. Only the beneficiary sees beneficiary_account_id, its credit and compliance_review; the partner sees a held transfer in its ordinary state. Credit only when creditable is true. Requires transactions:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string |
Responses
- 200 The transfer, as the caller may see it.
- 404 Unknown, or not the caller's (partner or beneficiary) in this mode.
not_found
Request
curl -X GET "$KULPAY_URL/v1/incoming-transfers/{id}" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"amount_minor": 125000,
"beneficiary_account_id": "3Ky2WEM0zMfI2pD5i2iBhSEQcB2",
"beneficiary_name": "L*** D*** L**",
"creditable": true,
"currency": "MZN",
"destination": {
"type": "deposit_reference",
"value": "INV-1001"
},
"final": true,
"final_at": "2026-10-11T09:45:00Z",
"id": "3KdW1pZ0incomingTransfer001",
"mode": "live",
"partner_reference": "BCI-TRF-20261011-0042",
"psp_id": "3KbHx5Qp7eYLffSWnjTF10m1LdZ",
"received_at": "2026-10-11T09:30:00Z",
"status": "confirmed_final",
"updated_at": "2026-10-11T09:45:00Z",
"value_date": "2026-10-11",
"version": 3
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such incoming transfer",
"error": {
"code": "not_found",
"message": "no such incoming transfer"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
POST /v1/incoming-transfers/{id}/credit
Report the credit of a transfer
The beneficiary (KulOffice) reports that it credited a transfer confirmed final, under its own credit_reference; the transfer becomes credited_notified (incoming_transfer.credited). The same reference again changes nothing. A matched transfer that is not final is 409 not_final; one in compliance review or returned is 409 invalid_state. Requires transactions:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string |
Request body application/json required
| Field | Type | Description |
|---|---|---|
credit_reference required | string |
Responses
Request
curl -X POST "$KULPAY_URL/v1/incoming-transfers/{id}/credit" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"credit_reference": "KO-TX-9F2A"
}'
200 Response application/json
{
"amount_minor": 125000,
"beneficiary_account_id": "3Ky2WEM0zMfI2pD5i2iBhSEQcB2",
"beneficiary_name": "L*** D*** L**",
"credit_reference": "KO-TX-9F2A",
"creditable": false,
"credited_at": "2026-10-11T09:46:10Z",
"currency": "MZN",
"destination": {
"type": "deposit_reference",
"value": "INV-1001"
},
"final": true,
"final_at": "2026-10-11T09:45:00Z",
"id": "3KdW1pZ0incomingTransfer001",
"mode": "live",
"partner_reference": "BCI-TRF-20261011-0042",
"psp_id": "3KbHx5Qp7eYLffSWnjTF10m1LdZ",
"received_at": "2026-10-11T09:30:00Z",
"status": "credited_notified",
"updated_at": "2026-10-11T09:46:10Z",
"value_date": "2026-10-11",
"version": 4
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such incoming transfer",
"error": {
"code": "not_found",
"message": "no such incoming transfer"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
409 Error · not_final application/problem+json
{
"code": "not_final",
"detail": "payments: the transfer is not confirmed final and cannot be credited",
"error": {
"code": "not_final",
"message": "payments: the transfer is not confirmed final and cannot be credited"
},
"status": 409,
"title": "Conflict",
"type": "about:blank"
}
POST /v1/incoming-transfers/{id}/finality
Confirm a transfer final
The reporting partner confirms the money is final. A matched transfer becomes confirmed_final (incoming_transfer.confirmed_final); one without a beneficiary goes to suspense (incoming_transfer.finality_confirmed). Confirming again changes nothing. Requires transactions:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string |
Responses
- 200 The transfer.
- 404 Unknown, or not the caller's (partner or beneficiary) in this mode.
not_found - 409 The transfer was returned.
invalid_state
Request
curl -X POST "$KULPAY_URL/v1/incoming-transfers/{id}/finality" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"amount_minor": 125000,
"beneficiary_name": "L*** D*** L**",
"creditable": true,
"currency": "MZN",
"destination": {
"type": "deposit_reference",
"value": "INV-1001"
},
"final": true,
"final_at": "2026-10-11T09:45:00Z",
"id": "3KdW1pZ0incomingTransfer001",
"mode": "live",
"partner_reference": "BCI-TRF-20261011-0042",
"psp_id": "3KbHx5Qp7eYLffSWnjTF10m1LdZ",
"received_at": "2026-10-11T09:30:00Z",
"status": "confirmed_final",
"updated_at": "2026-10-11T09:45:00Z",
"value_date": "2026-10-11",
"version": 3
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such incoming transfer",
"error": {
"code": "not_found",
"message": "no such incoming transfer"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
409 Error · invalid_state application/problem+json
{
"code": "invalid_state",
"detail": "the transfer was returned",
"error": {
"code": "invalid_state",
"message": "the transfer was returned"
},
"status": 409,
"title": "Conflict",
"type": "about:blank"
}