Skip to main content

Server-Side Integration

REST API Documentation · Version 1.5 · October 2026

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.

Flexible Schema

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:

  1. 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)
  2. Send the risk-context payload to Mubeen - POST the data to our validation endpoint
  3. Process response from Mubeen - Process the risk decision (log, allow, challenge, review, block) and take appropriate action
  4. 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
Use the host issued with your credentials

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
Your validate key is not your public_ site key

The 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​

Payload limits

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",
"user_email": "[email protected]",
"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​

ParameterTypeRequiredDescription
event_typestringRecommendedEvent 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
timestampstringOptionalISO 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_idstringOptionalYour transaction identifier (monetary events)
attempt_idstringOptionalYour identifier for this specific attempt (e.g., a single login or payment attempt)
order_idstringOptionalYour order identifier
external_idstringOptionalAny other external/internal reference id from your system
device_idstringOptionalDevice fingerprint ID from the Mubeen web or mobile SDK. Strongly recommended — it's the primary device signal for fraud detection
Your identifiers vs. Mubeen's request_id

Do 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​

ParameterTypeRequiredDescription
user_idstringRecommendedUnique customer/user identifier. If absent, phone_number is used, then user_email. Send it explicitly so the identity on the record is predictable
user_emailstringRecommendedCustomer email address
full_namestringOptionalCustomer full name
phone_numberstringOptionalCustomer phone number in international format (e.g., +966512345678)
account_created_atstringOptionalISO 8601 timestamp of when the customer account was created

Network & Device Fields​

ParameterTypeRequiredDescription
ip_addressstringRecommendedClient IP address for enhanced validation
ip_countrystringOptionalISO 3166-1 alpha-2 country code of the IP address (e.g., SA, AE, US)
user_agentstringRecommendedClient user agent string
channelstringOptionalChannel through which the event originated: app or web
platformstringOptionalPlatform of the client: ios, android, or web

Payment & Card Fields​

ParameterTypeRequiredDescription
payment_methodstringOptionalPayment method name (e.g., Visa, Mastercard, Mada, ApplePay, GooglePay)
card_binstringOptionalCard BIN (first 6-8 digits of the card number)
card_last4stringOptionalLast 4 digits of the card number
card_expiry_monthintegerOptionalCard expiry month (1-12)
card_expiry_yearintegerOptionalCard expiry year (e.g., 2028)
card_fingerprintstringOptionalUnique hash of the card for identifying the same card across transactions
card_brandstringOptionalCard brand identifier: visa, mastercard, mada, amex
card_issuing_bankstringOptionalName of the card issuing bank
card_issuer_countrystringOptionalISO 3166-1 alpha-2 country code of the card issuer (e.g., SA, AE)

Transaction Fields​

ParameterTypeRequiredDescription
transaction_amountnumberOptionalTransaction amount value (e.g., 149.99)
currencystringRecommendedISO 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_idstringOptionalIdentifier of the merchant the transaction belongs to (e.g., mer_a1b2c3)
merchant_categorystringOptionalMerchant category code or name (e.g., 5411)
beneficiary_idstringOptionalIdentifier of the beneficiary receiving the funds. Send it on transfer events
transaction_typestringOptionalYour sub-type for the transaction (e.g., p2p, topup)
product_ids_listarrayOptionalList of product names or identifiers in the transaction

Additional Fields​

ParameterTypeRequiredDescription
endpointstringOptionalThe endpoint path being accessed (e.g., /checkout/payment)
session_idstringOptionalSession identifier for tracking user sessions
billing_addressobjectOptionalBilling address: street, city, state, postal_code, country
shipping_addressobjectOptionalShipping address: street, city, state, postal_code, country
Custom fieldsanyOptionalAny 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 TypeCategoryDescription
paymentMonetaryPayment transaction
checkoutMonetaryCheckout / cart completion
transferMonetaryMoney transfer between accounts
refundMonetaryRefund processing
creditMonetaryCredit or top-up operation
signinNon-monetaryUser sign-in attempt
registrationNon-monetaryNew account registration
email_changeNon-monetaryAccount email address change
phone_changeNon-monetaryAccount phone number change
password_changeNon-monetaryAuthenticated password change
security_settings_changeNon-monetary2FA, trusted devices, recovery options
listingNon-monetaryContent or product listing creation
Monetary vs Non-monetary

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",
"user_email": "[email protected]",
"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",
"user_email": "[email protected]",
"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​

FieldTypeDescription
request_idstringMubeen-generated unique identifier for this validation request (UUID format). Use it, or the external_id you sent, to correlate feedback
decisionstringRecommended decision: allow, log, challenge, block, or review
risk_scorenumberRisk score from 0-100 (0=lowest risk, 100=highest risk)
risk_levelstringRisk category: low, medium, high, or critical
evaluation_time_msnumberServer-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 RangeRisk LevelTypical DecisionDescription
🟢 0-29lowlog or allowLegitimate user behavior, proceed normally
🟡 30-59mediumallow or challengeSlightly elevated risk, may require additional verification
🟠 60-84highchallenge or reviewSignificant risk indicators, verification or review recommended
🔴 85-100criticalblockHigh confidence of fraudulent activity, block action

Decision Types​

DecisionDescriptionRecommended Action
🟢 logVery low risk, log for analytics onlyProceed without restrictions
🟢 allowMatches customer-whitelisted patternProceed with standard flow
🟡 challengeMedium risk, additional verification neededRequest MFA, CAPTCHA, or email verification
🟠 reviewHigh risk, requires manual reviewQueue for human review before proceeding
🔴 blockCritical risk, prevent actionBlock 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"
}
FieldDescription
errorHuman-readable message. For display/logging only — do not parse it; the wording can change.
error_codeStable, machine-readable code. Branch on this (or the HTTP status), never on the error text.
timestampRFC 3339 UTC timestamp of when the error occurred.
request_idPer-request identifier (UUID). Also returned on the X-Request-ID response header. Quote it in support requests.

Error Codes​

HTTPerror_codeWhen
400INVALID_REQUESTBody is not valid JSON / could not be parsed.
400VALIDATION_FAILEDBody parsed but failed validation — missing required field, bad value, or a reserved key.
401UNAUTHORIZEDAPI key missing or invalid; tenant could not be resolved.
413PAYLOAD_TOO_LARGERequest body exceeds the configured size limit.
500INTERNAL_ERRORServer-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"
}
Rate limiting (429) is not emitted by this service

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​

ParameterTypeRequiredDescription
request_idstringRequiredEither Mubeen's request_id from the validation response, or the external_id you sent on the original request
outcomestringRequiredActual outcome: success, fail, fraud, dispute, chargeback, rejected, refund, captured, declined, victim_report
outcome_detailsobjectOptionalAdditional context about the outcome (e.g., fraud type, detection method)
external_idstringOptionalYour 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_id you 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, and order_id are 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​

OutcomeDescriptionWhen to Use
successTransaction completed successfully, no fraud detectedUser completed payment, registration was legitimate
failTransaction failed or login attempt failedGeneric non-fraud related failure
fraudConfirmed fraudulent activityManual review confirmed fraud, suspicious patterns detected
disputePayment was disputed by the cardholderCustomer filed a dispute with their bank or payment provider
chargebackPayment was reversed via chargebackCredit card chargeback received from the issuing bank
rejectedRequest was rejected by your systemFailed verification, blocked by your rules
refundTransaction was refundedCustomer-initiated or merchant-initiated refund processed
capturedPayment was capturedAn authorization was captured successfully
declinedPayment was declinedDeclined by the issuer or your payment provider
victim_reportThe customer reported being a fraud victimCustomer-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:

HTTPerror_codeWhen
400INVALID_REQUESTBody is not valid JSON.
400VALIDATION_FAILEDrequest_id or outcome is missing/blank, outcome is not one of the allowed values, or outcome_details is not a JSON object.
401UNAUTHORIZEDAPI key missing or invalid.
403ACCESS_DENIEDThe referenced request exists but belongs to a different tenant.
404REQUEST_NOT_FOUNDNo request matches request_id for your tenant.
500INTERNAL_ERRORPersisting 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​

  1. Send feedback as soon as possible - The sooner you provide feedback, the faster the ML models improve
  2. Include detailed outcome_details - More context helps train more accurate models
  3. Send feedback for all outcomes - Both successful and fraudulent transactions are valuable
  4. Use external_id for traceability - Including your internal order or transaction ID as external_id makes 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​

FieldTypeRequiredDescription
outcomestringRequiredThe 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_detailsobjectOptionalFree-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_recorded

feedback_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 decision in any way.
  • It is retained in the request's audit record for traceability.
  • The outcome is recorded against the same request_id that the call generates and returns — exactly as if you had followed up with a POST /feedback using that request_id.

Validation Errors​

Inline feedback is validated before any evaluation. A bad feedback block returns 400 VALIDATION_FAILED and the event is not scored:

ConditionMessage
feedback is not a JSON objectfeedback must be a JSON object
outcome missing or not in the allowed setoutcome must be one of: success, fail, fraud, dispute, chargeback, rejected, refund, captured, declined, victim_report
outcome_details present but not a JSON objectfeedback.outcome_details must be a JSON object
outcome_details cannot be serializedfeedback.outcome_details could not be serialized
When to use the standalone POST /feedback instead

Use 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
Do not allow or block on review

A 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_review state.
  • Sign-in / account change — keep the session in a pending/limited state (no sensitive actions) until resolved.
  • Registration / listing — create the record in a pending state, 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.

Provisioned per account

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​

FieldTypeDescription
request_idstringThe request_id from the original /validate response. Use it to find the held request
event_typestringThe event type of the original request
previous_decisionstringAlways review — the decision being resolved
decisionstringFinal decision: allow or block
risk_scorenumberUpdated risk score (0-100) after review
risk_levelstringUpdated risk level: low, medium, high, or critical
reviewed_atstringRFC 3339 UTC timestamp of when the review was resolved
review_notesstringOptional 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 }
Make callback handling idempotent

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 decisionAction
🟢 allowRelease the hold and complete normally (capture the payment, activate the account, publish the listing)
🔴 blockCancel 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.

Always verify before acting

Your callback URL is publicly reachable. Treat unsigned or invalid-signature requests as hostile — never resume a held request based on an unverified callback.