API Live

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

NameInTypeDescription
statusquerystring

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

NameInTypeDescription
id requiredpathstring

Request body application/json required

FieldTypeDescription
account_idstring
action requiredstring
One of: bind, release, return, compliance_review
reason requiredstring

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

NameInTypeDescription
partner_referencequerystring
statusquerystring 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

FieldTypeDescription
amount_minor requiredinteger
currency requiredstring
destination requiredobject
destination.type requiredstring
One of: deposit_reference, alias, virtual_account, payment_reference
destination.value requiredstring
finalboolean
partner_reference requiredstring Your own transfer reference.
value_datestring (date)

Responses

  • 200 The same report was recorded before: that transfer.
  • 201 Recorded now; status is matched, unmatched or, 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

NameInTypeDescription
id requiredpathstring

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

NameInTypeDescription
id requiredpathstring

Request body application/json required

FieldTypeDescription
credit_reference requiredstring

Responses

  • 200 The transfer, credited.
  • 404 Unknown, or not the caller's (partner or beneficiary) in this mode. not_found
  • 409 Not confirmed final (not_final), or a state that cannot be credited (invalid_state).

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

NameInTypeDescription
id requiredpathstring

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