Webhooks

This guide explains how to set up and use SEON Webhooks for real-time event notifications. It includes setup instructions, payload references, retry policies, troubleshooting, and best practices. 

 

Introduction

SEON Webhooks let you receive real-time notifications when important events occur in your SEON environment:

  • A transaction is processed
  • Transaction status changes
  • List updates (blacklist, whitelist, custom lists)
  • AML alerts (person & entity updates)
  • Identity verification (session completed, status updates)
  • An eKYC process finished

When any of these events occur, SEON sends an HTTP POST request with JSON payload to the configured webhook URL.

 

Key features

  • Multiple endpoints: 10 webhook endpoints per client; create/edit/delete from the UI.
  • Monitoring dashboard: Real-time log with event type, timestamp, endpoint, status; expandable request/response details; filters by event type, endpoint, status, time range. Logs are retained for 30 days.
  • Secure delivery: All payloads are signed with HMAC-SHA256.
  • Reliable delivery: Guaranteed at-least-once delivery with retries.
  • Idempotent events: Each event has a Request-Id header.


How to register a new webhook


To register a webhook, go to Settings→System→Webhooks.

  1. Click Create new.
  2. Enter name and URL.
  3. Select the event types
  4. Validation step to check that the URL works - you need to send 
    a HTTP 200 response code in order to save the webhook. The endpoint should be able to handle the ping check and return a successful response. This request is not signed with a signature.
  5. In case of a successful validation the webhook can be saved
  6. From that point on, you can see deliveries for that specific URL

Ping check:

{
    "event": "ping",
    "message": "Webhook URL validation check."
}

Reference implementation:

import { Request, Response } from "express";
function verifyWebhookSignature(
 rawPayload: string,
 signatureHeader: string,
 secret: string
): boolean {
 // existing implementation
 return true;
}
export function webhookHandler(req: Request, res: Response) {
 const secret = process.env.WEBHOOK_SECRET;

 // Get signature from SEON-Signature header
 const signatureHeader = req.header("SEON-Signature");
 const payload = JSON.parse(req.body);
 // 1) ping event → no signature verification
 if (payload.event === "ping") {
   return res.status(200).json({ ok: true, event: "pong" });
 }
 // 2) Other events → require and verify signature
 const isValid = verifyWebhookSignature(req.body.toString(), signatureHeader, secret);
 if (!isValid) {
   return res.status(403).json({ ok: false, error: "Invalid signature" });
 }
 // 3) Process the event here...
 return res.status(200).json({ ok: true });
}

Logs

The logs page shows every single delivery attempt of SEON's webhook service. Every single item on this page is an attempt to deliver a registered webhook event. If one event failed and at a later point had any number of retries, then for that ID you will be able to see every single retry attempt as an individual record on the logs page. You can see the request and the response for every single delivery attempt.


Testing webhook payloads


On the test tab, you can test every event payload. You simply provide a webhook URL and select the event you would like to see the test payload for, and we will send a test attempt. If you can see and monitor that webhook URL, you will be able to see instantly what future event structures will look like.


Webhook headers

Every webhook request includes these headers:

  • Request-Id: Unique per event — use it to prevent duplicate processing.
  • Content-Type: Always application/json.
  • User-Agent: seon/<version>.
  • Webhook-Schema-Version: Schema version (e.g., v1).
  • SEON-Signature: Includes timestamp, key ID, and HMAC signature.

 

Signature verification
 

Verify every webhook signature before processing:

import crypto from "node:crypto"

function verifyWebhookSignature(
  rawPayload: string,
  signatureHeader: string,
  secret: string
): boolean {
  // Parse the signature header
  const parts: Record<string, string> = {};
  const signatures: string[] = [];

  signatureHeader.split(',').forEach(part => {
    const [key, value] = part.split('=').map(s => s.trim());
    if (key === 'sig') {
      signatures.push(value);
    } else {
      parts[key] = value;
    }
  });

  const timestamp = parts.t;

  if (!timestamp || signatures.length === 0) {
    return false;
  }

  // Validate timestamp (optional but recommended)
  const MAX_AGE_SECONDS = 300; // 5 minutes
  const now = Math.floor(Date.now() / 1000);
  if (now - parseInt(timestamp) > MAX_AGE_SECONDS) {
    console.warn('Webhook timestamp too old');
    return false;
  }

  // Reconstruct the signed string
  const signedString = `${timestamp}.${rawPayload}`;

  // Compute expected signature
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(signedString)
    .digest('hex');

  // Try to verify against each signature (supports key rotation)
  for (const signature of signatures) {
    try {
      if (crypto.timingSafeEqual(
        Buffer.from(expectedSignature),
        Buffer.from(signature)
      )) {
        return true;
      }
    } catch (error) {
      // Length mismatch or other error, try next signature
      continue;
    }
  }

  return false;
}

 

Retry Policy

SEON guarantees at-least-once delivery via automatic retries.

retryCountDelay (seconds)Delay (hh:mm:ss)
06000:01:00
112000:02:00
224000:04:00
348000:08:00
496000:16:00
51,92000:32:00
63,84001:04:00
77,68002:08:00
815,36004:16:00


Retry Conditions

  • No retry: HTTP 200, 201, 202, 204, 4xx except 429
  • Retry: HTTP 429, 5xx, connection timeouts, network errors.
     

Troubleshooting

  • Signature verification failing? Check:
    • Correct signing key.
    • Timestamp tolerance.
    • Unmodified request body.
    • Character encoding.
  • Duplicate webhooks? Use Request-Id to deduplicate.
  • Not receiving events? Ensure your endpoint:
    • Is public and accessible.
    • Responds with valid status codes.
    • Supports HTTPS with SSL.
    • Accepts POST requests.


Best Practices

  • Always verify webhook signatures before processing.
  • Reply fast, do the processing later.
  • Implement idempotency using Request-Id headers.
  • Respond within 10 seconds to avoid retries.
  • Use HTTPS with valid SSL.
  • Log webhook events for debugging and monitoring.
     

Webhook events

Transaction status updates

Sent when a transaction’s status changes (e.g. moved to review, approved, or declined).

{
  "timestamp": "2017-08-30T13:47:42+00:00",
  "event": "transaction/status_update",
  "data": {
    "id": "e601f2dae8f9",
    "state": "REVIEW",
    "label": "Marked as review"
  },
}

Transaction processed

Triggered when a transaction is processed in SEON. The payload contains the complete Fraud API response, including scores, signals, and decision data.

Response
{
  "timestamp": "2025-02-03T09:11:39.000Z",
  "event": "transaction/processed",
  "data": {
    "request": {
      "config": {
        "response_fields": "id,state,fraud_score,proxy_score,applied_rules,calculation_time,seon_id,version,bin_details,device_details,phone_details,email_details,ip_details",
        "device_fingerprinting": true,
        "ip_api": true
      },
      "action_type": "purchase",
      "affiliate_id": "a0719",
      "affiliate_name": "AffiName",
      "avs_result": "Y",
      "billing_city": "Saint-Quentin",
      "billing_country": "FR",
      "billing_phone": "+3655549264",
      "billing_region": "FR",
      "billing_street": "15 Rue d'Alembert",
      "billing_street2": "Street 2",
      "billing_zip": "2100",
      "bonus_campaign_id": "",
      "brand_id": "",
      "card_bin": "521890",
      "card_expire": "2020-10",
      "card_fullname": "New Admin Cardname",
      "card_hash": "\u001a\t\u0000z\u000ei(&m\u0011\u0002^U|Mq<\u001e[\u0010\u001er!;K\n\u0001M\u001aSKQ",
      "card_last": "4180",
      "cvv_result": "",
      "status_3d": "",
      "details_url": "https://www.apple.com",
      "device_id": "device2019051401",
      "discount_code": "54321",
      "email": "fraudv2@fraud.com",
      "email_domain": "news.com",
      "gift": null,
      "gift_message": null,
      "ip": "2001:738::179:100a",
      "items": [
        {
          "item_category": "Phones",
          "item_id": "15995846",
          "item_name": "iPhone X 64GB",
          "item_price": "1000",
          "item_quantity": "3",
          "item_store": "Storename",
          "item_store_country": "US",
          "item_url": "https://www.apple.com/iphone/",
          "item_custom_fields": {
            "Color": "spacegray",
            "RAM": "16GB"
          }
        },
        {
          "item_category": "Phones",
          "item_id": "159987352",
          "item_name": "iPhone 8 Plus 64GB",
          "item_price": "1000",
          "item_quantity": "1",
          "item_store": "Storename",
          "item_store_country": "US",
          "item_url": "https://www.apple.com/iphone/",
          "item_custom_fields": {
            "Color": "spacegray",
            "RAM": "16GB"
          }
        }
      ],
      "merchant_country": "DE",
      "merchant_created_at": 1446370717,
      "merchant_id": "m0716",
      "merchant_category": "Sample Category",
      "order_memo": "OrderMemo",
      "password_hash": "4654re654er6v5er4v64",
      "payment_mode": "card",
      "payment_provider": "Sample Provider2",
      "receiver_fullname": "Sample Receiver",
      "receiver_bank_account": "Sample Bank",
      "sca_method": "",
      "phone_number": "+77085898927",
      "regulation": "",
      "session_id": "a8c84a5",
      "shipping_city": "Rome",
      "shipping_country": "IT",
      "shipping_fullname": "New Admin Usership",
      "shipping_method": "UPS",
      "shipping_phone": "+3655549263",
      "shipping_region": "IT",
      "shipping_street": "Via Trento, 22",
      "shipping_street2": "Street 2",
      "shipping_zip": "86100",
      "transaction_amount": 101,
      "transaction_currency": "GBP",
      "transaction_id": "",
      "transaction_type": "purchase",
      "user_account_status": "",
      "user_balance": 130,
      "user_city": "Leuven",
      "user_country": "BE",
      "user_created": 1446370717,
      "user_fullname": "Spring Boot",
      "user_id": "testUserId_0",
      "custom_fields": {
        "days_notto_board": -2,
        "days_to_board": 2,
        "departure_airport": "BUD",
        "is_intangible_item": true,
        "is_pay_on_delivery": false,
        "quickfiltertest": "isitworking"
      },
      "user_name": "4",
      "user_region": "BE",
      "user_street": "Strijdersstraat 43",
      "user_street2": "Street 2",
      "user_zip": "3000",
      "user_bank_name": "Sample Bank",
      "user_bank_account": "34524356464-234542355-2345234534",
      "user_verification_level": "",
      "user_dob": "1911-11-11"
    },
    "response": {
      "id": "test_tr_id",
      "state": "APPROVE",
      "fraud_score": 2,
      "bin_details": null,
      "version": null,
      "applied_rules": [
        {
          "id": "test_id",
          "name": "test_name",
          "operation": "+",
          "score": 1
        }
      ],
      "device_details": {
        "timezone": null,
        "private_mode": null,
        "useragent": null,
        "fonts": null,
        "plugins": null,
        "op_sys": null,
        "cookie_enabled": null,
        "screen": null,
        "avail_screen": null,
        "window_screen": null,
        "webrtc_count": null,
        "cookie_hash": null,
        "device_hash": null,
        "js_ip": null,
        "js_ip_country": null,
        "js_ip_isp": null,
        "browser_hash": null,
        "webrtc_ips": [],
        "webrtc_activated": null,
        "flash": null,
        "java": null,
        "plugins_hash": null,
        "fonts_hash": null,
        "plugin_names": [],
        "device_type": null,
        "font_names": [],
        "social_sites": [],
        "session_id": null,
        "type": "web",
        "dns_ip": null,
        "dns_ip_country": null,
        "dns_ip_isp": null
      },
      "calculation_time": 1,
      "seon_id": 1,
      "ip_details": {
        "ip": null,
        "score": 0,
        "country": null,
        "state_prov": null,
        "city": null,
        "timezone_offset": null,
        "isp_name": null,
        "latitude": null,
        "longitude": null,
        "type": null,
        "open_ports": null,
        "tor": null,
        "harmful": null,
        "vpn": null,
        "web_proxy": null,
        "public_proxy": null,
        "spam_number": null,
        "spam_urls": null,
        "id": null,
        "history": {
          "hits": null,
          "customer_hits": null,
          "first_seen": null,
          "last_seen": null
        },
        "flags": [
          {
            "note": null,
            "date": null,
            "industry": null
          }
        ]
      },
      "email_details": {
        "email": null,
        "score": 0,
        "deliverable": null,
        "domain_details": {
          "domain": null,
          "tld": null,
          "created": null,
          "updated": null,
          "expires": null,
          "registered": false,
          "registrar_name": null,
          "registered_to": null,
          "disposable": false,
          "free": false,
          "custom": true,
          "dmarc_enforced": false,
          "spf_strict": false,
          "valid_mx": null,
          "accept_all": null,
          "suspicious_tld": null,
          "website_exists": false
        },
        "account_details": {
          "apple": {
            "registered": null
          },
          "ebay": {
            "registered": null
          },
          "facebook": {
            "registered": null,
            "url": null,
            "name": null,
            "photo": null
          },
          "flickr": {
            "registered": null
          },
          "foursquare": {
            "registered": null
          },
          "github": {
            "registered": null
          },
          "google": {
            "registered": null,
            "photo": null
          },
          "gravatar": {
            "registered": null
          },
          "instagram": {
            "registered": null
          },
          "lastfm": {
            "registered": null
          },
          "linkedin": {
            "registered": null,
            "url": null,
            "name": null,
            "company": null,
            "title": null,
            "location": null,
            "website": null,
            "twitter": null,
            "photo": null
          },
          "microsoft": {
            "registered": null
          },
          "myspace": {
            "registered": null
          },
          "pinterest": {
            "registered": null
          },
          "skype": {
            "registered": null,
            "country": null,
            "country_code": null,
            "city": null,
            "gender": null,
            "name": null,
            "id": null,
            "handle": null,
            "bio": null,
            "age": null,
            "language": null,
            "state": null,
            "photo": null
          },
          "spotify": {
            "registered": null
          },
          "tumblr": {
            "registered": null
          },
          "twitter": {
            "registered": null
          },
          "vimeo": {
            "registered": null
          },
          "weibo": {
            "registered": null
          },
          "yahoo": {
            "registered": null
          },
          "discord": {
            "registered": null
          },
          "ok": {
            "registered": null,
            "city": null,
            "age": null,
            "date_joined": null
          },
          "kakao": {
            "registered": null
          },
          "booking": {
            "registered": null
          },
          "airbnb": {
            "registered": null,
            "about": null,
            "created_at": null,
            "first_name": null,
            "identity_verified": null,
            "location": null,
            "image": null,
            "reviewee_count": null,
            "trips": null,
            "work": null
          },
          "amazon": {
            "registered": null
          },
          "qzone": {
            "registered": null
          }
        },
        "breach_details": {
          "breaches": null,
          "haveibeenpwned_listed": null,
          "number_of_breaches": 0,
          "first_breach": null
        },
        "id": null,
        "history": {
          "hits": null,
          "customer_hits": null,
          "first_seen": null,
          "last_seen": null
        },
        "flags": []
      },
      "phone_details": {
        "number": 0,
        "valid": null,
        "type": null,
        "country": null,
        "carrier": null,
        "score": 0,
        "account_details": {
          "facebook": {
            "registered": null
          },
          "google": {
            "registered": null
          },
          "twitter": {
            "registered": null
          },
          "instagram": {
            "registered": null
          },
          "yahoo": {
            "registered": null
          },
          "microsoft": {
            "registered": null
          },
          "snapchat": {
            "registered": null
          },
          "skype": {
            "registered": null,
            "age": null,
            "city": null,
            "bio": null,
            "country": null,
            "country_code": null,
            "gender": null,
            "language": null,
            "name": null,
            "handle": null,
            "id": null,
            "photo": null,
            "state": null
          },
          "whatsapp": {
            "registered": null,
            "about": null,
            "photo": null,
            "last_seen": null
          },
          "telegram": {
            "registered": null,
            "photo": null,
            "last_seen": null
          },
          "viber": {
            "registered": null,
            "name": null,
            "photo": null,
            "last_seen": null
          },
          "kakao": {
            "registered": null
          },
          "ok": {
            "registered": null
          },
          "zalo": {
            "registered": null
          },
          "line": {
            "registered": null,
            "name": null,
            "photo": null
          }
        },
        "id": null,
        "history": {
          "hits": null,
          "customer_hits": null,
          "first_seen": null,
          "last_seen": null
        },
        "flags": [
          {
            "note": null,
            "date": null,
            "industry": null
          }
        ]
      }
    }
  },
}

Blacklist / Whitelist updates

Triggered when a user or data point is added to or removed from a blacklist or whitelist. Sent when a transaction’s status changes (e.g. moved to review, approved, or declined).

Response
{
  "timestamp": "2017-08-30T13:47:42+00:00",
  "event": "lists/blacklist-whitelist",
  "data": {
    "data_field": "user_id",
    "value": "111",
    "state": "blacklist"
  },
}

Custom list updates

Fired when a record is updated in a custom list, such as a watchlist or internal tracking list.

Response
{
  "timestamp": "2017-08-30T13:47:42+00:00",
  "event": "lists/customlist",
  "data": {
    "data_field": "user_id",
    "value": "413132231",
    "state": "watchlist"
  },
}

AML Person updates

Notifies you when AML screening results for a person change (e.g. new PEP or sanctions hit).

Response
{
  "timestamp": "2017-08-30T13:47:42+00:00",
  "event": "aml/person_updates",
  "data": {
    "hash": "9d42a989bffc31bd1330a36229f4aa2e480be4de65ecfa291b896baaf73938a2",
    "user_id": "212313213",
    "status": "REOPENED",
    "changes": [
      "pep",
      "sanctionlist",
      "watchlist",
      "crimelist"
    ]
  },
}

AML Entity Updates

Notifies you of AML status changes for entities such as companies or organizations.

Response
{
 "timestamp": "2017-08-30T13:47:42+00:00",
 "event": "aml/entity_updates",
 "data": {
   "hash": "9d42a989bffc31bd1330a36229f4aa2e480be4de65ecfa291b896baaf73938a2",
   "user_id": "212313213",
   "status": "REOPENED",
   "changes": [
     "sanctionlist",
     "watchlist"
   ]
 },
}

Identity Verification – session finished

Indicates that an identity verification session has been completed and a final decision is available.

Response
{
 "event": "idv/session_finished",
 "timestamp": "2025-07-10T14:30:00Z",
 "data": {
  "status": "APPROVED",
  "statusDetail": "Verification completed successfully.",
  "platform": "WEB",
  "sessionId": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
  "userId": "98765",
  "name": "Jane Doe"
 }
}

Identity Verification – status updated

Sent when an ongoing IDV session’s status changes (e.g. moved to review or pending).

Response
{
  "event": "idv/session_status_updated",
  "timestamp": "2025-07-10T15:05:12Z",
  "data": {
    "status": "REVIEW",
    "statusDetail": "Document image quality is too low.",
    "platform": "IOS",
    "sessionId": "f0e9d8c7-b6a5-4321-fedc-ba9876543210",
    "userId": "54321",
    "email": "john.smith@example.com"
  }
}

eKYC finished

Triggered when an eKYC verification flow has completed, providing the outcome and verification details.

Response
{
 "timestamp": "2025-02-03T09:11:39.000Z",
 "event": "ekyc/finished",
 "data": {
   "type": "AADHAAR",
   "status": "SUCCESS",
   "userReferenceId": "test",
   "verificationResults": {
     "aadhaar": true
   }
 },
}