Response
Webhook Interaction
This section details how the transaction screening API communicates its results via webhooks. It outlines the structure of the webhook requests, the authentication measures in place, and the expected response handling on your side.
Webhook Overview
- Purpose: Webhooks are used to notify your system asynchronously once the transaction screening process is complete.
- Trigger: A webhook request is triggered when the transaction screening pipeline completes processing.
- Authentication: To ensure the authenticity of the webhook, each request includes a cryptographic signature in the
x-exo-signatureheader.
Webhook Authentication
Webhook requests are signed using the SHA256 hashing algorithm with a shared secret. The signature allows you to verify that the webhook request originated from the API.
How to Verify the Signature
- Compute a SHA256 hash of the request body using the shared secret provided during webhook configuration.
- Compare the computed hash with the value in the
x-exo-signatureheader.
Here’s an example:
- Shell
- Node.js
- Python
- Ruby
- Go
#!/bin/bash
request_body='{"key":"value"}'
secret="your-secret"
received_signature="received-signature"
computed_signature=$(echo -n "$request_body" | openssl dgst -sha256 -hmac "$secret" | awk '{print $2}')
if [ "$computed_signature" = "$received_signature" ]; then
echo "Signatures match"
else
echo "Signatures do not match"
fi
const crypto = require('crypto');
function verifySignature(requestBody, receivedSignature, secret) {
const computedSignature = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(requestBody))
.digest('hex');
return computedSignature === receivedSignature;
}
import hmac
import hashlib
import json
def verify_signature(request_body, received_signature, secret):
computed_signature = hmac.new(
secret.encode(),
json.dumps(request_body).encode(),
hashlib.sha256
).hexdigest()
return computed_signature == received_signature
# Example usage
request_body = {"key": "value"}
secret = "your-secret"
received_signature = "received-signature"
if verify_signature(request_body, received_signature, secret):
print("Signatures match")
else:
print("Signatures do not match")
require 'openssl'
require 'json'
def verify_signature(request_body, received_signature, secret)
computed_signature = OpenSSL::HMAC.hexdigest('sha256', secret, JSON.dump(request_body))
computed_signature == received_signature
end
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
)
func verifySignature(requestBody map[string]interface{}, receivedSignature, secret string) bool {
requestJSON, _ := json.Marshal(requestBody)
h := hmac.New(sha256.New, []byte(secret))
h.Write(requestJSON)
computedSignature := hex.EncodeToString(h.Sum(nil))
return computedSignature == receivedSignature
}
func main() {
requestBody := map[string]interface{}{
"key": "value",
}
secret := "your-secret"
receivedSignature := "received-signature"
if verifySignature(requestBody, receivedSignature, secret) {
fmt.Println("Signatures match")
} else {
fmt.Println("Signatures do not match")
}
}
Webhook Request Structure
Below is an example of the JSON payload sent to your webhook endpoint:
- Completed Screening
- Failed Screening
- Completed Review
{
"eventType": "TransactionScreeningCompleted",
"eventTimestamp": "2024-11-07T12:50:18.928Z",
"eventVersion": "v1.0",
"content": {
"transactionId": "672cb77f3327348d86c66e30",
"timestamp": "2024-11-07T12:50:18.928Z",
"riskScore": 0.6,
"overallDecision": "MANUAL_REVIEW" # or APPROVE,
"clientRuleCheck": {
"rulesMatched": ["Flag transactions that are above 10,000 USD in value": true]
},
"blacklistCheck": {
"senderStatus": { "isBlacklisted": false },
"receiverStatus": { "isBlacklisted": false },
"deviceStatus": { "isBlacklisted": false },
"userStatus": { "isBlacklisted": false }
},
"aiRiskScoring": {
"score": 0.6,
"decision": "MANUAL_REVIEW",
"comment": "The user is classified as high risk due to anomalous behaviors such as a long gap since last login, unusual transaction timing and intervals, and atypical spending patterns for the day of the week, with a risk score of 0.6."
},
"transactionDetails": {
"transactionData": {
"reference": "w9ryn91ri7e2p6e7rtq9",
"amount": 160434.99,
"receiverAccount": "41227666026",
"senderAccount": "7839522916",
"isExternalPayment": true,
"status": "completed",
"balanceBefore": 467353.19,
"type": "credit",
"channel": "bank transfer",
"transactionDate": "2024-09-02T00:00:00+00:00",
"vasReceiver": "kw5k93jd5f",
"currency": "USD",
"narration": "Subscription fee"
},
"anonymizedUserData": {
"uniqueId": "6e5b040e-01b5-4086-8d1a-137e9a125008",
"isBanned": false,
"businessCategory": "no business",
"isPhoneNumberVerified": true,
"accountType": "individual",
"dateJoined": "2018-08-20",
"age": 29,
"isIdentityVerified": true,
"state": "california",
"city": "san francisco",
"country": "United States of America"
},
"location": {
"latitude": 24.5763,
"longitude": -82.4142
}
}
}
}
{
"eventType": "TransactionScreeningFailed",
"eventTimestamp": "2024-11-07T12:50:18.928Z",
"eventVersion": "v1.0",
"content": {
"transactionId": "672cb77f3327348d86c66e30",
"timestamp": "2024-11-07T12:50:18.928Z",
"transactionDetails": {
"transactionData": {
"reference": "w9ryn91ri7e2p6e7rtq9",
"amount": 160434.99,
"receiverAccount": "41227666026",
"senderAccount": "7839522916",
"isExternalPayment": true,
"status": "completed",
"balanceBefore": 467353.19,
"type": "credit",
"channel": "bank transfer",
"transactionDate": "2024-09-02T00:00:00+00:00",
"vasReceiver": "kw5k93jd5f",
"currency": "USD",
"narration": "Subscription fee"
},
"anonymizedUserData": {
"uniqueId": "6e5b040e-01b5-4086-8d1a-137e9a125008",
"isBanned": false,
"businessCategory": "no business",
"isPhoneNumberVerified": true,
"accountType": "individual",
"dateJoined": "2018-08-20",
"age": 29,
"isIdentityVerified": true,
"state": "california",
"city": "san francisco",
"country": "United States of America"
},
"location": {
"latitude": 24.5763,
"longitude": -82.4142
}
}
}
}
{
"eventType": "TransactionReviewCompleted",
"eventTimestamp": "2024-11-07T12:50:18.928Z",
"eventVersion": "v1.0",
"content": {
"reviewAction":"TransactionApproved" # or TransactionRejected
"transactionId": "672cb77f3327348d86c66e30",
"timestamp": "2024-11-07T12:50:18.928Z",
"reason": "analyst approved,
"analystId": "41227666026",
"transactionDetails": {
"transactionData": {
"reference": "w9ryn91ri7e2p6e7rtq9",
"amount": 160434.99,
"receiverAccount": "41227666026",
"senderAccount": "7839522916",
"isExternalPayment": true,
"status": "completed",
"balanceBefore": 467353.19,
"type": "credit",
"channel": "bank transfer",
"transactionDate": "2024-09-02T00:00:00+00:00",
"vasReceiver": "kw5k93jd5f",
"currency": "USD",
"narration": "Subscription fee"
},
"anonymizedUserData": {
"uniqueId": "6e5b040e-01b5-4086-8d1a-137e9a125008",
"isBanned": false,
"businessCategory": "no business",
"isPhoneNumberVerified": true,
"accountType": "individual",
"dateJoined": "2018-08-20",
"age": 29,
"isIdentityVerified": true,
"state": "california",
"city": "san francisco",
"country": "United States of America"
},
"location": {
"latitude": 24.5763,
"longitude": -82.4142
}
}
}
}
Webhook Response Handling
Your webhook endpoint should respond with an HTTP status code to indicate the success or failure of the request processing.
- Success: Respond with
200 OKto acknowledge the webhook. - Failure: Respond with any 4xx or 5xx status code to indicate a failure. The system will retry failed webhooks.
Retry Mechanism
The system attempts to retry failed webhook deliveries up to 3 times, with exponential backoff. Ensure your webhook endpoint is resilient and capable of handling retries.
Security Recommendations
- Keep your shared secret confidential to ensure secure webhook authentication.
- Log and monitor all incoming webhook requests to detect unauthorized attempts.
- Verify the
x-exo-signatureheader for every incoming request.