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
| Field | Type | Description |
|---|---|---|
list required | string | One of: beneficiary, phone, card |
reason required | string | |
value required | string |
Responses
- 201 Added.
- 400 Missing fields or already listed.
invalid_request
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
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string | |
reason | query | string |
Responses
- 204 Removed.
- 404 No such entry.
not_found
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
| Name | In | Type | Description |
|---|---|---|---|
status | query | string | 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
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string |
Request body application/json required
| Field | Type | Description |
|---|---|---|
approve required | boolean | |
reason required | string |
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
| Field | Type | Description |
|---|---|---|
reason required | string | |
review_timeout_minutes | integer | |
rules required | object[] | |
rules[].action required | string | One of: review, decline, challenge |
rules[].countries | string[] | |
rules[].currencies | string[] | |
rules[].id required | string | |
rules[].key | string | One of: phone, card, account |
rules[].list | string | One of: beneficiary, phone, card |
rules[].max_amount | string | |
rules[].max_count | integer | |
rules[].methods | string[] | |
rules[].min_amount | string | |
rules[].type required | string | One of: velocity, amount, country, blocklist |
rules[].window_seconds | integer |
Responses
- 202 Prepared.
- 400 Invalid rules, each problem listed.
invalid_request
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"
}
KulPay