KulPayAPI Live

API reference

Risk

GET /v1/admin/risk

Risk rules in force

Every transaction passes a risk stage before routing (PSP-6.3). The built-in engine applies these rules; the most severe action of the rules that fire decides: decline (refused before any provider call, 422 risk_declined; the caller is not told which rule), review (held pending for a compliance officer; it expires after review_timeout_minutes, default 1440), challenge (proceeds; the connector is asked to step up authentication via metadata kulpay_challenge=required), else approve. Each decision is recorded with the rule ids that fired (transaction.risk.approved, review_required, declined, challenged). Velocity counts phones, cards and accounts as hashes. Requires admin:read.

Responses

  • 200 Active rules, versions, pending approvals.

Request

curl -X GET "$KULPAY_URL/v1/admin/risk" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "active": {
    "rules": [
      {
        "action": "review",
        "id": "big",
        "min_amount": "1000",
        "type": "amount"
      }
    ]
  },
  "active_version": 1,
  "versions": [
    {
      "activated_at": "2026-10-10T22:15:43.556916651Z",
      "approval_id": "3KWPqlI8eTHLPeIyqzeOkflh7Tm",
      "created_at": "2026-10-10T22:15:43.531498263Z",
      "created_by": "key_accounts",
      "id": "3KWPqnzI7EnerX06CYufMI2Dvlf",
      "reason": "hold large payments",
      "status": "active",
      "version": 1
    }
  ]
}

GET /v1/admin/risk/blocklist

The risk block list

Entries are kept as hashes; only a label (the last digits) is shown.

Responses

  • 200 Entries.

Request

curl -X GET "$KULPAY_URL/v1/admin/risk/blocklist" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "data": [
    {
      "created_at": "2026-10-10T22:15:43.755506637Z",
      "created_by": "key_accounts",
      "id": "3KWPqlGMif6gHkmvCvziGSIU3Nu",
      "label": "…4567",
      "list": "phone",
      "reason": "fraud report"
    }
  ],
  "total": 1
}

POST /v1/admin/risk/blocklist

Block a beneficiary, phone or card

Request body application/json required

FieldTypeDescription
list requiredstring
One of: beneficiary, phone, card
reason requiredstring
value requiredstring

Responses

Request

curl -X POST "$KULPAY_URL/v1/admin/risk/blocklist" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "list": "phone",
  "reason": "fraud report",
  "value": "+258 84 123 4567"
}'
201 Response application/json
{
  "created_at": "2026-10-10T22:15:43.755506637Z",
  "created_by": "key_accounts",
  "id": "3KWPqlGMif6gHkmvCvziGSIU3Nu",
  "label": "…4567",
  "list": "phone",
  "reason": "fraud report"
}
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "the value is already on the phone list",
  "error": {
    "code": "invalid_request",
    "message": "the value is already on the phone list"
  },
  "status": 400,
  "title": "Bad Request",
  "type": "about:blank"
}

DELETE /v1/admin/risk/blocklist/{id}

Unblock an entry

Parameters

NameInTypeDescription
id requiredpathstring
reasonquerystring

Responses

Request

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

GET /v1/admin/risk/reviews

Payments held for review

Parameters

NameInTypeDescription
statusquerystring
One of: open, approved, declined, expired

Responses

  • 200 Reviews, oldest first, with the rules that fired and the due time.

Request

curl -X GET "$KULPAY_URL/v1/admin/risk/reviews" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "data": [
    {
      "account_id": "3KWPqoZJjidjdKEJj56o2fPtD9Y",
      "created_at": "2026-10-10T22:15:43.61286086Z",
      "due_at": "2026-10-11T22:15:43.61286086Z",
      "id": "3KWPqk1L2HZGjVZNlguB7EJOnVo",
      "mode": "test",
      "payment_id": "3KWPqo4UmMf7pgnGgcnWvrWFihC",
      "reasons": "[\"amount 5000 MZN is at or above 1000\"]",
      "rules": "[\"big\"]",
      "status": "open"
    }
  ],
  "total": 1
}

POST /v1/admin/risk/reviews/{id}/decide

Approve or decline a held payment

Approve: the payment is routed and submitted. Decline: it fails (transaction_not_allowed). Recorded as transaction.risk.review_decided.

Parameters

NameInTypeDescription
id requiredpathstring

Request body application/json required

FieldTypeDescription
approve requiredboolean
reason requiredstring

Responses

  • 200 {payment_id, status}.
  • 404 No such review. not_found

Request

curl -X POST "$KULPAY_URL/v1/admin/risk/reviews/{id}/decide" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "approve": true,
  "reason": "Pagamento de energia, outubro"
}'
200 Response application/json
{
  "payment_id": "3KUYN1JYs7zLC4MODFfSw8UqHA6",
  "status": "failed"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such review",
  "error": {
    "code": "not_found",
    "message": "no such review"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

POST /v1/admin/risk/versions

Prepare a risk rules version

Active once a second person approves it (risk.activate). A rule of type limit (F3-9) caps bill payments per payer_phone (E.164, counted across channels), reference or channel, over a window of day or month (Maputo time), by max_count and/or max_amount (one currency); its action is decline and a refusal answers 422 limit_exceeded with rule. suspicion: true on any rule opens a compliance case when it fires (F3-8).

Request body application/json required

FieldTypeDescription
reason requiredstring
review_timeout_minutesinteger
rules requiredobject[]
rules[].action requiredstring
One of: review, decline, challenge
rules[].countriesstring[]
rules[].currenciesstring[]
rules[].id requiredstring
rules[].keystring
One of: phone, card, account
rules[].liststring
One of: beneficiary, phone, card
rules[].max_amountstring
rules[].max_countinteger
rules[].methodsstring[]
rules[].min_amountstring
rules[].type requiredstring
One of: velocity, amount, country, blocklist
rules[].window_secondsinteger

Responses

Request

curl -X POST "$KULPAY_URL/v1/admin/risk/versions" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "reason": "hold large payments",
  "rules": [
    {
      "action": "review",
      "id": "big",
      "min_amount": "1000",
      "type": "amount"
    }
  ]
}'
202 Response application/json
{
  "approval": {
    "created_at": "2026-10-10T22:15:43.5330168Z",
    "id": "3KWPqlI8eTHLPeIyqzeOkflh7Tm",
    "kind": "risk.activate",
    "prepared_by": "key_accounts",
    "reason": "risk rules v1: hold large payments",
    "status": "pending",
    "subject_id": "3KWPqnzI7EnerX06CYufMI2Dvlf",
    "tenant_id": "platform"
  },
  "message": "risk rules version prepared; a second person must approve it",
  "version": {
    "approval_id": "3KWPqlI8eTHLPeIyqzeOkflh7Tm",
    "created_at": "2026-10-10T22:15:43.531498263Z",
    "created_by": "key_accounts",
    "id": "3KWPqnzI7EnerX06CYufMI2Dvlf",
    "reason": "hold large payments",
    "status": "pending",
    "version": 1
  }
}
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "rule 2: an id is required; velocity_phone: window_seconds is 1 to 31 days",
  "error": {
    "code": "invalid_request",
    "message": "rule 2: an id is required; velocity_phone: window_seconds is 1 to 31 days"
  },
  "status": 400,
  "title": "Bad Request",
  "type": "about:blank"
}