Approve a held payment
The risk.reviews.approve operation: POST /api/v1/risk/reviews/{id}/approve in the REST API and the risk_reviews_approve tool in the MCP server.
/api/v1/risk/reviews/{id}/approveReleases a payment the risk rules held: the customer's wallets the hold froze are unfrozen (unless another
review of the customer is still pending, or someone changed the wallet since), the hold's reason leaves the
customer's risk (back to normal when nothing else elevated it), and risk.review.approved goes to your
webhook endpoints, so your integration can deliver what it held back. A review is decided once.
At a glance
- REST:
POST /api/v1/risk/reviews/{id}/approve - MCP tool:
risk_reviews_approve - Operation ID:
risk.reviews.approve - Scope: Brand
- Permission:
customers:write - Risk tier:
write - Dry run: Supported (send
Ownrecord-Dry-Run: true) - Idempotency:
Idempotency-Keyrequired - Modes: Test and live
- Authentication: API key with the
customers:writescope - Since: M9
Request
Path parameters
| Field | Type | Description |
|---|---|---|
id | rsk_… ID |
Body
A JSON object (Content-Type: application/json):
| Field | Type | Required | Description |
|---|---|---|---|
note | string | No | |
expectedVersion | integer (at least 1) | No |
Response
200 OK with a JSON object:
| Field | Type | Description |
|---|---|---|
id | rsk_… ID | |
object | "risk_evaluation" | |
livemode | boolean | |
brandId | brd_… ID | |
subject | object | |
subject.type | one of checkout_session, auto_reload | |
subject.checkoutSessionId | cs_… ID or null | |
subject.walletId | wal_… ID or null | |
customerId | cus_… ID or null | |
paymentId | pay_… ID or null | |
stage | one of intent, payment_method, confirm, off_session | |
decision | one of allow, require_3ds, review, block | |
score | integer | |
reasons | array of object | |
reasons[].code | one of 21 values, e.g. customer_blocked, customer_elevated, blocklist_email (all in the OpenAPI document) | |
reasons[].action | one of block, review, require_3ds, score | |
reasons[].points | integer or null | |
reasons[].detail | string | |
signals | object | |
signals.emailDomain | string or null | |
signals.ipNetwork | string or null | |
signals.ipCountry | string or null | |
signals.billingCountry | string or null | |
signals.cardCountry | string or null | |
signals.cardFingerprint | string or null | |
signals.amount | money or null | |
signals.topup | boolean | |
signals.newCustomer | boolean | |
signals.firstPayment | boolean | |
signals.disposableEmail | boolean | |
history | array of object | |
history[].at | date-time | |
history[].stage | one of intent, payment_method, confirm, off_session | |
history[].decision | one of allow, require_3ds, review, block | |
history[].score | integer | |
review | object or null | |
createdAt | date-time | |
updatedAt | date-time |
Examples
Approve after calling the customer
POST /api/v1/risk/reviews/rsk_01m4ttwd3veh9apbewxheq5xr1/approve
Authorization: Bearer or_test_…
Idempotency-Key: 3f1c9a52-7d0e-4b8f-9a61-0c2d5e8b7f14
Content-Type: application/json
{
"note": "Called the customer; the card is a company card."
}
Response (200):
{
"id": "rsk_01m4ttwd3veh9apbewxheq5xr1",
"object": "risk_evaluation",
"livemode": true,
"brandId": "brd_01m4ttwd3veh9apbewxheq5xh5",
"subject": {
"type": "checkout_session",
"checkoutSessionId": "cs_01m4ttwd3veh9apbewxheq5xr4",
"walletId": null
},
"customerId": "cus_01m4ttwd3veh9apbewxheq5xr5",
"paymentId": "pay_01m4ttwd3veh9apbewxheq5xr6",
"stage": "confirm",
"decision": "review",
"score": 70,
"reasons": [
{
"code": "first_payment",
"action": "require_3ds",
"points": null,
"detail": "The customer's first payment."
},
{
"code": "new_customer",
"action": "score",
"points": 10,
"detail": "A new customer."
},
{
"code": "country_mismatch_ip_billing",
"action": "score",
"points": 20,
"detail": "IP country DE isn't the billing country HR."
},
{
"code": "country_mismatch_card_billing",
"action": "score",
"points": 25,
"detail": "Card country US isn't the billing country HR."
},
{
"code": "country_mismatch_ip_card",
"action": "score",
"points": 15,
"detail": "IP country DE isn't the card country US."
},
{
"code": "score_review",
"action": "review",
"points": null,
"detail": "Score 70 ≥ 70."
}
],
"signals": {
"emailDomain": "corp.example",
"ipNetwork": "203.0.113.0",
"ipCountry": "DE",
"billingCountry": "HR",
"cardCountry": "US",
"cardFingerprint": "Xo1abC9dEf2GhI3j",
"amount": {
"amount": "90000000",
"currency": "EUR"
},
"topup": true,
"newCustomer": true,
"firstPayment": true,
"disposableEmail": false
},
"history": [
{
"at": "2026-10-20T09:00:00.000Z",
"stage": "intent",
"decision": "require_3ds",
"score": 30
},
{
"at": "2026-10-20T09:01:00.000Z",
"stage": "payment_method",
"decision": "review",
"score": 70
},
{
"at": "2026-10-20T09:01:30.000Z",
"stage": "confirm",
"decision": "review",
"score": 70
}
],
"review": {
"status": "approved",
"openedAt": "2026-10-20T09:02:00.000Z",
"decidedAt": "2026-10-20T11:30:00.000Z",
"decidedBy": "usr_01m4ttwd3veh9apbewxheq5xh6",
"note": "Called the customer; the card is a company card.",
"refundId": null,
"blocklistEntryIds": [],
"frozenWalletIds": ["wal_01m4ttwd3veh9apbewxheq5xr7"]
},
"createdAt": "2026-10-20T09:00:00.000Z",
"updatedAt": "2026-10-20T11:30:00.000Z"
}
Errors
| Code | HTTP status | When | What to do |
|---|---|---|---|
resource_not_found | 404 | No risk evaluation with this ID in this brand and mode. | Check the ID and that your credentials are scoped to the right organization, entity, brand and mode. |
risk_evaluation_not_held | 400 | The evaluation held no payment for review. | Only payments the risk rules held are in the review queue (risk.reviews.list). Refund other payments with refunds.create. |
risk_review_not_pending | 409 | The review was already approved or rejected. | Reload the review: it was approved or rejected (details.status). A decision is final; refund the payment or unfreeze the wallet by hand if something must change. |
version_conflict | 409 | The review changed since expectedVersion, or at the same moment. | Fetch the object again and retry the change. |
permission_denied | 403 | You can't change this brand's customers. | Ask an organization owner or admin for a role or scope that allows this action. |
It can also return these common errors: validation_failed, api_version_unsupported, invalid_json, unsupported_media_type, authentication_required, api_key_invalid, scope_required, idempotency_key_required, idempotency_key_reused, idempotency_in_progress, approval_required, approval_rejected, approval_expired, stale_approval, approval_policy_denied, rate_limited, internal_error.