Server-Side Integration
This guide will help you integrate the Mubeen Risk engine into your backend services. Your server sends a risk-context payload—user, transaction, payment, and network data—to Mubeen's servers for a risk decision. One field in that payload, the device ID, is collected by our web or mobile SDK and forwarded from your frontend; the rest comes from your own systems.
The request schema shown in this guide is a generic default that works for most use cases. If your data uses different field names or you need custom fields or validation logic, Mubeen adapts the schema on our side to match your existing data format—so you can send data in your current structure without reshaping your systems to fit ours.
Integration Overview
Server-side integration requires the following steps:
- Assemble the request data - Gather your user, transaction, payment, and network data, plus the device ID collected by the Mubeen web or mobile SDK on your client (passed to your backend via form or API)
- Send the risk-context payload to Mubeen - POST the data to our validation endpoint
- Process response from Mubeen - Process the risk decision (log, allow, challenge, review, block) and take appropriate action
- Send Feedback to Mubeen (Optional) - Report the outcome of the transaction for improved accuracy
How it works: Your server assembles a risk-context payload from your own systems — user, transaction, payment, and network data — and sends it to the Mubeen API for scoring. The one piece it can't produce on its own is the device ID, which your frontend obtains from the Mubeen web or mobile SDK and passes to your backend to include in the payload.
Request validation Endpoint
POST https://api.sa.mubeen.ai/api/v1/generic/validate
Mubeen runs isolated regional instances, and your account lives on exactly one of them. The host above is the KSA instance (api.sa.mubeen.ai); an EU account uses api.de.mubeen.ai, and the Zain Cloud deployment uses api.efm.zaincloud.com.sa. Your keys are bound to your instance and will be rejected with 401 by the others, so use the host issued alongside your credentials rather than choosing one.
Note: HTTP/2.0 is supported for improved performance and efficiency. Gzip compression is also supported for better performance.
Authentication
Include your API key in the request headers:
X-API-Key: <your_key>
Content-Type: application/json
public_ site keyThe X-API-Key value is your server-side validate key, issued separately from the public_ site key used by the JavaScript SDK. They are different credentials — get the validate key from your Mubeen dashboard or account manager.
Quick Test (cURL)
Fire your first request in a single command. For an initial smoke test you can pass a placeholder device_id — in production this value comes from the JavaScript SDK or Android SDK.
curl -X POST https://api.sa.mubeen.ai/api/v1/generic/validate \
-H "X-API-Key: <your_key>" \
-H "Content-Type: application/json" \
-d '{
"event_type": "signin",
"timestamp": "2025-03-15T10:30:00Z",
"attempt_id": "att_login_8841",
"external_id": "session_aabbcc",
"device_id": "fp_test_placeholder",
"user_email": "[email protected]",
"ip_address": "203.0.113.42"
}'
A successful call returns a decision object — see Response Format below.
Request Format
The request body must be valid JSON and is subject to these limits: at most 200 keys, 20 levels of nesting, and a maximum body size (defaults to roughly 20 MB; exceeding it returns 413 PAYLOAD_TOO_LARGE). The top-level key internal is reserved for system use — including it returns 400 VALIDATION_FAILED.
HTTP Request
POST /api/v1/generic/validate HTTP/1.1
Host: api.sa.mubeen.ai
X-API-Key: <your_key>
Content-Type: application/json
{
"event_type": "payment",
"timestamp": "2025-03-15T10:30:00Z",
"trx_id": "trx_019a54474a4e",
"attempt_id": "att_001",
"order_id": "order_12345",
"external_id": "order_12345",
"device_id": "fp_1234567890abcdef",
"user_id": "usr_123456",
"full_name": "Ahmed Al-Rashid",
"phone_number": "+966512345678",
"ip_address": "203.0.113.42",
"ip_country": "SA",
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0",
"payment_method": "Visa",
"card_bin": "42424242",
"card_last4": "4242",
"card_expiry_month": 6,
"card_expiry_year": 2028,
"card_fingerprint": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"card_brand": "visa",
"card_issuing_bank": "Al Rajhi Banking & Inv. Corp.",
"card_issuer_country": "SA",
"transaction_amount": 149.99,
"currency": "USD",
"merchant_id": "mer_a1b2c3",
"account_created_at": "2024-01-01T00:00:00Z",
"product_ids_list": ["PUBGs", "Fortnite"],
"channel": "app",
"platform": "ios"
}
Request Parameters
Core Fields
| Parameter | Type | Required | Description |
|---|---|---|---|
event_type | string | Recommended | Event type being validated. See Event Types below. If omitted it defaults to checkout. The value is not validated against the list below, so a typo is accepted silently and creates a new event type on your account |
timestamp | string | Optional | ISO 8601 timestamp of when the event occurred (e.g., 2025-03-15T10:30:00Z). If omitted, the server's current time is used. A timestamp in the future is clamped to the time of receipt |
trx_id | string | Optional | Your transaction identifier (monetary events) |
attempt_id | string | Optional | Your identifier for this specific attempt (e.g., a single login or payment attempt) |
order_id | string | Optional | Your order identifier |
external_id | string | Optional | Any other external/internal reference id from your system |
device_id | string | Optional | Device fingerprint ID from the Mubeen web or mobile SDK. Strongly recommended — it's the primary device signal for fraud detection |
request_idDo not send a request_id in the request body — Mubeen generates one and returns it in the response. Include whichever of trx_id, attempt_id, order_id, and external_id apply to the event; they are retained on the stored request (they are not echoed back in the response).
To correlate feedback later you must use either Mubeen's request_id or your external_id. Those are the only two values feedback can be matched on. trx_id, attempt_id, and order_id are stored but are not lookup keys, so put the value you intend to send feedback with in external_id.
Customer Fields
| Parameter | Type | Required | Description |
|---|---|---|---|
user_id | string | Recommended | Unique customer/user identifier. If absent, phone_number is used, then user_email. Send it explicitly so the identity on the record is predictable |
user_email | string | Recommended | Customer email address |
full_name | string | Optional | Customer full name |
phone_number | string | Optional | Customer phone number in international format (e.g., +966512345678) |
account_created_at | string | Optional | ISO 8601 timestamp of when the customer account was created |
Network & Device Fields
| Parameter | Type | Required | Description |
|---|---|---|---|
ip_address | string | Recommended | Client IP address for enhanced validation |
ip_country | string | Optional | ISO 3166-1 alpha-2 country code of the IP address (e.g., SA, AE, US) |
user_agent | string | Recommended | Client user agent string |
channel | string | Optional | Channel through which the event originated: app or web |
platform | string | Optional | Platform of the client: ios, android, or web |
Payment & Card Fields
| Parameter | Type | Required | Description |
|---|---|---|---|
payment_method | string | Optional | Payment method name (e.g., Visa, Mastercard, Mada, ApplePay, GooglePay) |
card_bin | string | Optional | Card BIN (first 6-8 digits of the card number) |
card_last4 | string | Optional | Last 4 digits of the card number |
card_expiry_month | integer | Optional | Card expiry month (1-12) |
card_expiry_year | integer | Optional | Card expiry year (e.g., 2028) |
card_fingerprint | string | Optional | Unique hash of the card for identifying the same card across transactions |
card_brand | string | Optional | Card brand identifier: visa, mastercard, mada, amex |
card_issuing_bank | string | Optional | Name of the card issuing bank |
card_issuer_country | string | Optional | ISO 3166-1 alpha-2 country code of the card issuer (e.g., SA, AE) |
Transaction Fields
| Parameter | Type | Required | Description |
|---|---|---|---|
transaction_amount | number | Optional | Transaction amount value (e.g., 149.99) |
currency | string | Recommended | ISO 4217 currency code of the transaction (e.g., USD). Not enforced by the API. If omitted, the amount is treated as USD, so a non-USD amount is converted wrongly and USD-based rules and reporting will be off |
merchant_id | string | Optional | Identifier of the merchant the transaction belongs to (e.g., mer_a1b2c3) |
merchant_category | string | Optional | Merchant category code or name (e.g., 5411) |
beneficiary_id | string | Optional | Identifier of the beneficiary receiving the funds. Send it on transfer events |
transaction_type | string | Optional | Your sub-type for the transaction (e.g., p2p, topup) |
product_ids_list | array | Optional | List of product names or identifiers in the transaction |
Additional Fields
| Parameter | Type | Required | Description |
|---|---|---|---|
endpoint | string | Optional | The endpoint path being accessed (e.g., /checkout/payment) |
session_id | string | Optional | Session identifier for tracking user sessions |
billing_address | object | Optional | Billing address: street, city, state, postal_code, country |
shipping_address | object | Optional | Shipping address: street, city, state, postal_code, country |
| Custom fields | any | Optional | Any additional contextual data relevant to your use case |
Event Types
For complete request and feedback examples for each event type, see Event Type Examples.
| Event Type | Category | Description |
|---|---|---|
payment | Monetary | Payment transaction |
checkout | Monetary | Checkout / cart completion |
transfer | Monetary | Money transfer between accounts |
refund | Monetary | Refund processing |
credit | Monetary | Credit or top-up operation |
signin | Non-monetary | User sign-in attempt |
registration | Non-monetary | New account registration |
email_change | Non-monetary | Account email address change |
phone_change | Non-monetary | Account phone number change |
password_change | Non-monetary | Authenticated password change |
security_settings_change | Non-monetary | 2FA, trusted devices, recovery options |
listing | Non-monetary | Content or product listing creation |
Monetary event types (payment, checkout, transfer, refund, credit) typically include a non-zero transaction_amount. Non-monetary event types will typically have a transaction_amount of 0 or omit the transaction_amount field entirely.
Example: Payment Event
{
"event_type": "payment",
"timestamp": "2025-03-15T10:30:00Z",
"trx_id": "trx_a1b2c3d4e5f6",
"attempt_id": "att_001",
"order_id": "order_98765",
"external_id": "order_98765",
"device_id": "fp_9876543210fedcba",
"user_id": "usr_123456",
"full_name": "Ahmed Al-Rashid",
"phone_number": "+966512345678",
"ip_address": "203.0.113.42",
"ip_country": "SA",
"user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)",
"payment_method": "Mada",
"card_bin": "42424242",
"card_last4": "4242",
"card_expiry_month": 6,
"card_expiry_year": 2028,
"card_fingerprint": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"card_brand": "mada",
"card_issuing_bank": "Al Rajhi Banking & Inv. Corp.",
"card_issuer_country": "SA",
"transaction_amount": 149.99,
"currency": "USD",
"merchant_id": "mer_a1b2c3",
"account_created_at": "2024-01-01T00:00:00Z",
"product_ids_list": ["PUBGs", "Fortnite"],
"channel": "app",
"platform": "ios"
}
Example: Sign-in Event
{
"event_type": "signin",
"timestamp": "2025-03-15T14:22:00Z",
"attempt_id": "att_login_7720",
"external_id": "session_ddeeff",
"device_id": "fp_9876543210fedcba",
"user_id": "usr_123456",
"full_name": "Ahmed Al-Rashid",
"ip_address": "203.0.113.42",
"ip_country": "SA",
"user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)",
"channel": "app",
"platform": "ios"
}
Response Format
Success Response
Status Code: 200 OK
{
"request_id": "019a5447-4a4e-7b3f-9562-54ce36825c2e",
"decision": "log",
"risk_score": 0,
"risk_level": "low",
"evaluation_time_ms": 12
}
Response Fields
| Field | Type | Description |
|---|---|---|
request_id | string | Mubeen-generated unique identifier for this validation request (UUID format). Use it, or the external_id you sent, to correlate feedback |
decision | string | Recommended decision: allow, log, challenge, block, or review |
risk_score | number | Risk score from 0-100 (0=lowest risk, 100=highest risk) |
risk_level | string | Risk category: low, medium, high, or critical |
evaluation_time_ms | number | Server-side evaluation time in milliseconds. Informational; safe to ignore, but do not reject the response for containing it |
Risk Score Interpretation
Note: Risk scores are calculated using machine learning models trained on your customer-specific data, continuously improving accuracy based on your unique patterns and behaviors.
| Score Range | Risk Level | Typical Decision | Description |
|---|---|---|---|
| 🟢 0-29 | low | log or allow | Legitimate user behavior, proceed normally |
| 🟡 30-59 | medium | allow or challenge | Slightly elevated risk, may require additional verification |
| 🟠 60-84 | high | challenge or review | Significant risk indicators, verification or review recommended |
| 🔴 85-100 | critical | block | High confidence of fraudulent activity, block action |
Decision Types
| Decision | Description | Recommended Action |
|---|---|---|
🟢 log | Very low risk, log for analytics only | Proceed without restrictions |
🟢 allow | Matches customer-whitelisted pattern | Proceed with standard flow |
🟡 challenge | Medium risk, additional verification needed | Request MFA, CAPTCHA, or email verification |
🟠 review | High risk, requires manual review | Queue for human review before proceeding |
🔴 block | Critical risk, prevent action | Block immediately and log security event |
Error Responses
Every non-2xx response uses one consistent envelope:
{
"error": "Invalid request body: unexpected end of JSON input",
"error_code": "INVALID_REQUEST",
"timestamp": "2026-06-01T22:30:00Z",
"request_id": "0190f3a1-2b4c-7d8e-9f01-23456789abcd"
}
| Field | Description |
|---|---|
error | Human-readable message. For display/logging only — do not parse it; the wording can change. |
error_code | Stable, machine-readable code. Branch on this (or the HTTP status), never on the error text. |
timestamp | RFC 3339 UTC timestamp of when the error occurred. |
request_id | Per-request identifier (UUID). Also returned on the X-Request-ID response header. Quote it in support requests. |
Error Codes
| HTTP | error_code | When |
|---|---|---|
| 400 | INVALID_REQUEST | Body is not valid JSON / could not be parsed. |
| 400 | VALIDATION_FAILED | Body parsed but failed validation — missing required field, bad value, or a reserved key. |
| 401 | UNAUTHORIZED | API key missing or invalid; tenant could not be resolved. |
| 413 | PAYLOAD_TOO_LARGE | Request body exceeds the configured size limit. |
| 500 | INTERNAL_ERROR | Server-side failure. The cause is logged under request_id and is never returned to the client. |
Example: 401 Unauthorized
{
"error": "Unauthorized: missing or invalid authentication",
"error_code": "UNAUTHORIZED",
"timestamp": "2026-06-01T22:30:00Z",
"request_id": "0190f3a1-2b4c-7d8e-9f01-23456789abcd"
}
The validation service does not return an HTTP 429. Velocity and rate limiting are treated as fraud signals that influence the decision in a normal 200 response. Any 429 you receive comes from the API gateway in front of the service — not from the error envelope above.
Feedback Endpoint
After processing a request and determining its actual outcome (whether successful or failed), you should send feedback to Mubeen to improve the accuracy of the ML models. This is an optional but highly recommended step that helps the system learn from real-world outcomes.
Endpoint
POST https://api.sa.mubeen.ai/api/v1/feedback
When to Send Feedback
Send feedback after you've processed the user's request and know the actual outcome. Feedback should be sent for both successful and failed requests:
Successful Outcomes:
- ✅ User completed payment successfully
- ✅ Registration was legitimate and verified
- ✅ Login attempt was successful
- ✅ Transaction processed without issues
Failed/Fraudulent Outcomes:
- ❌️ Request was rejected by your system
- ❌️ Fraud confirmed through manual review
- ❌️ Payment was disputed by the cardholder
- ❌️ Payment was charged back by the issuing bank
- ❌️ Transaction was refunded
Request Format
POST /api/v1/feedback
X-API-Key: <your_key>
Content-Type: application/json
{
"request_id": "019a5447-4a4e-7b3f-9562-54ce36825c2e",
"outcome": "success",
"outcome_details": {},
"external_id": "order_12345"
}
Feedback Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
request_id | string | Required | Either Mubeen's request_id from the validation response, or the external_id you sent on the original request |
outcome | string | Required | Actual outcome: success, fail, fraud, dispute, chargeback, rejected, refund, captured, declined, victim_report |
outcome_details | object | Optional | Additional context about the outcome (e.g., fraud type, detection method) |
external_id | string | Optional | Your external/internal identifier for back-referencing the original transaction |
Note: The request_id can be:
- Mubeen's
request_id: The UUID returned from/generic/validate(e.g.,019a5447-4a4e-7b3f-9562-54ce36825c2e) - Your own identifier: the
external_idyou sent on the request (e.g.,order_12345). This is the only one of your own identifiers that feedback can be matched on;trx_id,attempt_id, andorder_idare stored with the request but cannot be looked up
Using your external_id is convenient when you don't want to store Mubeen's request_id alongside your transaction data.
Outcome Types
| Outcome | Description | When to Use |
|---|---|---|
success | Transaction completed successfully, no fraud detected | User completed payment, registration was legitimate |
fail | Transaction failed or login attempt failed | Generic non-fraud related failure |
fraud | Confirmed fraudulent activity | Manual review confirmed fraud, suspicious patterns detected |
dispute | Payment was disputed by the cardholder | Customer filed a dispute with their bank or payment provider |
chargeback | Payment was reversed via chargeback | Credit card chargeback received from the issuing bank |
rejected | Request was rejected by your system | Failed verification, blocked by your rules |
refund | Transaction was refunded | Customer-initiated or merchant-initiated refund processed |
captured | Payment was captured | An authorization was captured successfully |
declined | Payment was declined | Declined by the issuer or your payment provider |
victim_report | The customer reported being a fraud victim | Customer-reported fraud, distinct from fraud you detected |
Example: Payment Success Feedback
{
"request_id": "019a5447-4a4e-7b3f-9562-54ce36825c2e",
"outcome": "success",
"external_id": "order_12345"
}
Example: Fraud Detection Feedback
{
"request_id": "019a5447-4a4e-7b3f-9562-54ce36825c2e",
"outcome": "fraud",
"outcome_details": {
"fraud_type": "stolen_card",
"detection_method": "manual_review",
"confidence": "high"
},
"external_id": "order_12345"
}
Example: Dispute Feedback
{
"request_id": "019a5447-4a4e-7b3f-9562-54ce36825c2e",
"outcome": "dispute",
"outcome_details": {
"dispute_reason": "unauthorized_transaction",
"dispute_date": "2025-04-01"
},
"external_id": "order_12345"
}
Response Format
Status Code: 200 OK
Unlike errors, a successful feedback call returns its own shape — not the error envelope:
{ "success": true, "message": "Feedback recorded successfully" }
Feedback Error Responses
Feedback errors use the same envelope as the validation endpoint (error + error_code + timestamp + request_id). Common cases:
| HTTP | error_code | When |
|---|---|---|
| 400 | INVALID_REQUEST | Body is not valid JSON. |
| 400 | VALIDATION_FAILED | request_id or outcome is missing/blank, outcome is not one of the allowed values, or outcome_details is not a JSON object. |
| 401 | UNAUTHORIZED | API key missing or invalid. |
| 403 | ACCESS_DENIED | The referenced request exists but belongs to a different tenant. |
| 404 | REQUEST_NOT_FOUND | No request matches request_id for your tenant. |
| 500 | INTERNAL_ERROR | Persisting the feedback record failed. |
Example: 404 Request Not Found
{
"error": "Request not found",
"error_code": "REQUEST_NOT_FOUND",
"timestamp": "2026-06-01T22:30:00Z",
"request_id": "0190f3a1-2b4c-7d8e-9f01-23456789abcd"
}
Best Practices
- Send feedback as soon as possible - The sooner you provide feedback, the faster the ML models improve
- Include detailed outcome_details - More context helps train more accurate models
- Send feedback for all outcomes - Both successful and fraudulent transactions are valuable
- Use
external_idfor traceability - Including your internal order or transaction ID asexternal_idmakes it easy to correlate Mubeen feedback with your own records
Inline Feedback (on Validate)
When you already know the outcome at submission time, you can submit the event and its outcome in a single call by adding an optional feedback object to the validate payload. This avoids the two-step validate → capture request_id → POST /feedback round-trip and is ideal for replaying or backfilling labeled data.
Request
Add a top-level feedback object alongside your normal event fields:
POST /api/v1/generic/validate
X-API-Key: <your_key>
Content-Type: application/json
{
"event_type": "checkout",
"user_id": "usr_123456",
"transaction_amount": 120.0,
"currency": "USD",
"feedback": {
"outcome": "chargeback",
"outcome_details": {
"reason": "stolen card",
"network_case_id": "CB-99182"
}
}
}
feedback Fields
| Field | Type | Required | Description |
|---|---|---|---|
outcome | string | Required | The known result. One of the same values accepted by the Feedback Endpoint: success, fail, fraud, dispute, chargeback, rejected, refund, captured, declined, victim_report (case-insensitive, trimmed). |
outcome_details | object | Optional | Free-form JSON object stored as the feedback context. Must be a JSON object if present. |
Example (cURL)
curl -X POST https://api.sa.mubeen.ai/api/v1/generic/validate \
-H "X-API-Key: <your_key>" \
-H "Content-Type: application/json" \
-d '{
"event_type": "checkout",
"user_id": "usr_123456",
"transaction_amount": 120.0,
"feedback": {
"outcome": "chargeback",
"outcome_details": { "reason": "stolen card" }
}
}'
Response
The event is evaluated exactly like a normal validate call, and the response gains a feedback_accepted flag:
{
"request_id": "019a5447-4a4e-7b3f-9562-54ce36825c2e",
"decision": "block",
"risk_score": 92,
"risk_level": "critical",
"evaluation_time_ms": 12,
"feedback_accepted": true
}
feedback_accepted, not feedback_recordedfeedback_accepted: true confirms the outcome was validated and queued, not that it is already durably stored. The outcome is recorded asynchronously and retried automatically on transient failure, so the flag means "accepted for recording," not "already persisted."
How the feedback block is handled
- It is validated and removed before scoring, so it never influences the risk evaluation or the returned
decisionin any way. - It is retained in the request's audit record for traceability.
- The outcome is recorded against the same
request_idthat the call generates and returns — exactly as if you had followed up with aPOST /feedbackusing thatrequest_id.
Validation Errors
Inline feedback is validated before any evaluation. A bad feedback block returns 400 VALIDATION_FAILED and the event is not scored:
| Condition | Message |
|---|---|
feedback is not a JSON object | feedback must be a JSON object |
outcome missing or not in the allowed set | outcome must be one of: success, fail, fraud, dispute, chargeback, rejected, refund, captured, declined, victim_report |
outcome_details present but not a JSON object | feedback.outcome_details must be a JSON object |
outcome_details cannot be serialized | feedback.outcome_details could not be serialized |
POST /feedback insteadUse inline feedback when the outcome is known at submission time (replay / backfill). For outcomes learned after the transaction — a chargeback that lands days later — use the standalone Feedback Endpoint against the original request_id.
Handling the review Decision (Asynchronous Callback)
Unlike every other decision, review is not a final answer. It means Mubeen needs more time — typically manual analyst review or additional automated checks — before committing to an outcome. Your integration must place the request on hold and wait for Mubeen to call back with a final decision of either allow or block.
validate → "decision": "review" ──▶ put request on hold
│
(manual / automated review) │ (seconds to hours)
▼
your callback URL ◀── POST final decision (allow | block) ── Mubeen
│
▼
resume the held request
reviewA review response is provisional. Do not complete the action and do not reject it outright — hold it until the callback arrives. Acting early defeats the purpose of the review.
1. Place the request on hold
When the validate response returns "decision": "review", pause the user-facing action and persist the request_id so you can match the later callback to the held request:
- Payment / checkout / transfer — authorize but do not capture, or move the order to a
pending_reviewstate. - Sign-in / account change — keep the session in a pending/limited state (no sensitive actions) until resolved.
- Registration / listing — create the record in a
pendingstate, not yet visible or active.
2. Configure your callback URL
Mubeen delivers review resolutions to a single HTTPS endpoint registered for your account. Set the callback URL and retrieve your callback signing secret from your Mubeen dashboard or account manager.
The callback URL and signing secret are configured on Mubeen's side, the same way your validate key is issued. They are not set through the validation API.
3. Receive the resolution callback
Once the review concludes, Mubeen sends an HTTP POST to your callback URL with the final decision:
POST https://your-app.example.com/mubeen/callback HTTP/1.1
Content-Type: application/json
X-Mubeen-Signature: t=1733130900,v1=5257a869e7e...c8f2a
{
"request_id": "019a5447-4a4e-7b3f-9562-54ce36825c2e",
"event_type": "payment",
"previous_decision": "review",
"decision": "block",
"risk_score": 88,
"risk_level": "critical",
"reviewed_at": "2026-06-02T09:15:00Z",
"review_notes": "Manual review confirmed stolen card"
}
Callback Fields
| Field | Type | Description |
|---|---|---|
request_id | string | The request_id from the original /validate response. Use it to find the held request |
event_type | string | The event type of the original request |
previous_decision | string | Always review — the decision being resolved |
decision | string | Final decision: allow or block |
risk_score | number | Updated risk score (0-100) after review |
risk_level | string | Updated risk level: low, medium, high, or critical |
reviewed_at | string | RFC 3339 UTC timestamp of when the review was resolved |
review_notes | string | Optional human-readable context from the reviewer |
4. Acknowledge the callback
Respond with 200 OK as soon as you've durably recorded the decision. Any non-2xx response (or a timeout) is treated as a delivery failure, and Mubeen retries with exponential backoff.
{ "success": true }
A callback may be delivered more than once (e.g., after a retry). Key on request_id and ignore a resolution you have already applied, so a duplicate delivery never double-processes the held request.
5. Resume the held request
Apply the final decision to the request you put on hold:
Final decision | Action |
|---|---|
🟢 allow | Release the hold and complete normally (capture the payment, activate the account, publish the listing) |
🔴 block | Cancel or reject the held request and log a security event |
After resuming, send feedback for the request as usual so the outcome feeds back into the models.
Verifying the callback signature
Every callback carries an X-Mubeen-Signature header so you can confirm it genuinely came from Mubeen. Compute an HMAC-SHA256 over the raw request body using your callback signing secret and compare it (in constant time) to the signature in the header. Reject any request whose signature does not match.
Your callback URL is publicly reachable. Treat unsigned or invalid-signature requests as hostile — never resume a held request based on an unverified callback.