# Guide to Socure Webhooks: Features and Best Practices

Socure webhooks tell your systems the outcome of identity verification and fraud checks. When a RiskOS evaluation finishes with a decision, pauses for a document or one-time passcode, or fails to run, and when a reviewer changes a decision, marks fraud or updates a case, RiskOS can notify your application so onboarding, account access or manual review moves on without polling.

This guide covers how RiskOS webhooks are configured and authenticated, the events and the envelope they share, the retry rules, and the practices that keep a Socure receiver correct and secure.

## What are Socure webhooks?

RiskOS is Socure's platform for running identity and fraud workflows. A RiskOS webhook belongs to one use case, such as consumer onboarding, and subscribes to one or more event types. When a subscribed event happens, RiskOS sends a JSON `POST` to your HTTPS endpoint, wrapped in a standard envelope with an `event_id`, `event_at`, `event_type` and a `data` object.

RiskOS does not sign webhooks. Each webhook is created with one of three authentication methods, and RiskOS includes the credentials in every request: **Basic authentication** (a username and password in an `Authorization: Basic` header), a **Bearer token** (a UUID in `Authorization: Bearer`), or **OAuth 2.0** (RiskOS fetches a fresh token from your token URL before each delivery). This guide focuses on Basic authentication, which is what Hookdeck's Socure source type checks.

## Socure webhook features

| Feature | Details |
| --- | --- |
| Configuration | RiskOS Dashboard > **Developer Workbench** > **Webhooks** > **Add a webhook**, or the RiskOS CLI |
| Fields | Name, URL, use case, event types, auth type, status (`ACTIVE` or `INACTIVE`) |
| Endpoint | HTTPS with TLS 1.2 or higher, accepting `POST` and `application/json` |
| Authentication | Basic, Bearer (UUID) or OAuth 2.0 client credentials, chosen at creation |
| Changing credentials | Not possible. Delete and recreate the webhook to rotate or switch |
| Signature and timestamp | None. `event_at` is in the body but is not signed |
| Envelope | `event_id`, `event_at`, `event_type`, `data` |
| Response | `2xx` within 30 seconds |
| Retries | Up to 10 attempts, exponential backoff capped at 60 seconds, attempts 6 to 10 a minute apart |
| Retried status codes | `504`, `503`, `502`, `429`, `423`, `409`, `408` |
| Duplicates | Possible, even for successfully delivered events |
| Custom headers | Static header variables, set up through your Solutions Consultant |
| Source IPs | Published for sandbox and production |
| Testing | **Continue to Test** sends a sample payload before you save |

## Common events

| Group | Event types | Sent when |
| --- | --- | --- |
| Evaluation | `evaluation_completed`, `evaluation_paused`, `reevaluation`, `workflow_execution_failed` | An evaluation finishes with a final decision, pauses for async input such as DocV or OTP, is reevaluated, or fails to execute |
| Case management | `decision_update`, `fraud_confirming`, `review_case_assigned`, `review_case_unassigned`, `case_status_updated`, `case_notes_added`, `case_attachment_added` | A reviewer changes a decision, a case is marked as fraud or non-fraud, assigned or unassigned, changes status, or gets notes or attachments |
| Watchlist | `watchlist_notification_monitored_search_updated`, `case_management_notification_case_closure` | A monitored entity is added to or removed from a watchlist, or a watchlist case is approved or closed |

Which events you can select depends on your account's entitlements. Here is Socure's `case_status_updated` example, trimmed:

```json
{
  "event_id": "82ef8e08-24b4-42c6-9e27-f7b9aa892c58",
  "event_at": "2023-09-14T07:46:55.956104Z",
  "event_type": "case_status_updated",
  "data": {
    "id": "abc-test-123",
    "workflow": "consumer_onboarding",
    "eval_id": "d3a15e54-ed6b-43f5-8fea-8f6370cc89d9",
    "reviewer_id": "test2@abc.com",
    "decision": "REVIEW",
    "status": "OPEN",
    "sub_status": "Awaiting Documentation",
    "updated_at": "2023-09-14T07:46:55.956018Z",
    "environment_name": "Sandbox"
  }
}

```

`data.id` is your own identifier for the evaluation and `data.eval_id` is RiskOS's. For evaluation events, `data` follows the same schema as the Evaluation API response, including `decision` and any `data_enrichments`, so it can contain sensitive personal data.

> **Inspect Socure webhooks as they arrive.** Point a sandbox webhook at a [Hookdeck Inspect](https://inspect.hookdeck.com) URL and use **Continue to Test** to see the envelope and headers, with no account or setup required. Keep real customer data out of any third-party test tool.

## Setting up Socure webhooks

Choose a username and a long random password first, for example with `openssl rand -hex 32` for the password. You cannot change them later without recreating the webhook, so store them in your secrets manager before you start.

1. In the RiskOS Dashboard, go to **Developer Workbench** > **Webhooks** and click **Add a webhook**.
2. Enter a name and your HTTPS endpoint URL.
3. Choose the **Use Case** the webhook applies to.
4. Select the event types you need.
5. Set **Auth Type** to **Basic Authentication** and enter the username and password.
6. Click **Continue to Test** to send a sample payload, then **Save** to activate the webhook.

If you restrict inbound traffic by IP, allowlist Socure's addresses before testing, or the test can fail. Note that the two Socure pages that list them disagree: the set-up guide lists three addresses each for sandbox and production, while the configuration reference lists four for sandbox and five for production, as CIDR ranges. Socure says it notifies customers before changing them, so confirm the current list with Socure Support.

To route through Hookdeck, create a Hookdeck source with the **Socure** type and use its URL in step 2. The source asks for a **Username** and **Password**: enter the same pair. It accepts `POST`.

The Hookdeck source checks HTTP Basic credentials only, so a webhook created with a Bearer token or OAuth 2.0 will not verify. Hookdeck also never answers with a `401` challenge, so RiskOS has to send the credentials with the first request. Socure's docs say RiskOS includes the configured credentials in every outbound request, but do not describe the exchange in more detail, so check the test delivery in Hookdeck to confirm it arrived with an `Authorization: Basic` header.

## Securing Socure webhooks

Decode the `Authorization: Basic` header, split on the first colon, and compare the username and password with your stored values in constant time.

```javascript
const crypto = require('crypto');
const express = require('express');

const USERNAME = process.env.SOCURE_WEBHOOK_USERNAME;
const PASSWORD = process.env.SOCURE_WEBHOOK_PASSWORD;

function safeEqual(a, b) {
  // timingSafeEqual throws on unequal lengths, so check first.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

function verifyBasicAuth(header) {
  if (!USERNAME || !PASSWORD || typeof header !== 'string') return false;
  const match = /^basic\s+([A-Za-z0-9+/]+={0,2})\s*$/i.exec(header);
  if (!match) return false;
  const decoded = Buffer.from(match[1], 'base64');
  const colon = decoded.indexOf(0x3a); // split on the first colon
  if (colon === -1) return false;
  const userOk = safeEqual(decoded.subarray(0, colon), Buffer.from(USERNAME, 'utf8'));
  const passOk = safeEqual(decoded.subarray(colon + 1), Buffer.from(PASSWORD, 'utf8'));
  return userOk && passOk;
}

const app = express();

app.post('/webhooks/socure', express.raw({ type: '*/*' }), (req, res) => {
  if (!verifyBasicAuth(req.get('authorization'))) {
    return res.sendStatus(401);
  }

  let event;
  try {
    event = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }

  // Deduplicate on event.event_id, then route on event.event_type.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import base64
import binascii
import hmac
import os

USERNAME = os.environ.get("SOCURE_WEBHOOK_USERNAME", "")
PASSWORD = os.environ.get("SOCURE_WEBHOOK_PASSWORD", "")

def verify_basic_auth(header) -> bool:
    if not USERNAME or not PASSWORD or not header:
        return False
    scheme, _, encoded = header.partition(" ")
    if scheme.lower() != "basic":
        return False
    try:
        decoded = base64.b64decode(encoded.strip(), validate=True)
    except (binascii.Error, ValueError):
        return False
    user, sep, password = decoded.partition(b":")
    if not sep:
        return False
    user_ok = hmac.compare_digest(user, USERNAME.encode("utf-8"))
    pass_ok = hmac.compare_digest(password, PASSWORD.encode("utf-8"))
    return user_ok and pass_ok

```

Basic credentials are static. They authenticate the sender but not the body, so a modified payload still passes, and `event_at` is not signed, so a captured request can be replayed. Use HTTPS, which RiskOS requires, a long random password, and allowlist Socure's IPs as a second layer. Treat payloads as untrusted, as Socure recommends, and store only the data you need.

> **Make Socure webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) checks the Basic credentials you configure, responds inside RiskOS's 30-second window, and keeps every evaluation and case event so none is lost to an outage.

## Socure webhook limitations and pain points

### Credentials are fixed for the life of the webhook

**The Problem:** Rotating a leaked password, or switching from Basic to another method, means deleting the webhook and creating a new one.

**Why It Happens:** RiskOS does not let you change a webhook's auth type, credentials or custom headers after creation. Editing only updates the URL and event subscriptions.

**Workarounds:**

* Create the replacement webhook first with the new credentials, accept both sets in your receiver briefly, then delete the old webhook.
* Keep a record of every webhook's use case and events so you can recreate it exactly.

**How Hookdeck Can Help:** Point every RiskOS webhook at a Hookdeck source. Rotation then means creating a new source and webhook, while your own service's URL and configuration stay the same behind Hookdeck.

### Only some failures are retried

**The Problem:** A `500` from a bug, or a `401` from a credentials mismatch, is not retried, and the event is gone.

**Why It Happens:** RiskOS retries only `504`, `503`, `502`, `429`, `423`, `409` and `408`. Other errors end delivery for that event.

**Workarounds:**

* Return `503` rather than `500` for transient failures in your own code.
* Reconcile against the RiskOS API for evaluations you expected to hear about.

**How Hookdeck Can Help:** Hookdeck stores each request and delivers to your service with [retries](/docs/retries) on your schedule, whatever your service returns. With Basic credentials configured, Hookdeck answers RiskOS with a `200` either way, records requests without matching credentials as failed verification, and never forwards them.

### Only Basic fits the source type

**The Problem:** A webhook created with a Bearer token or OAuth 2.0 never verifies in Hookdeck's Socure source.

**Why It Happens:** RiskOS supports three auth types, and Hookdeck's Socure source type checks HTTP Basic credentials only. The auth type cannot be changed after creation.

**Workarounds:**

* Create the webhook with **Basic Authentication** if you plan to use Hookdeck's Socure source.
* If you are committed to Bearer or OAuth 2.0, check the token in your own receiver.

**How Hookdeck Can Help:** With Basic Authentication, the Socure source compares the username and password for every request, so your service only receives authenticated events.

### Duplicates and sensitive payloads

**The Problem:** The same event can arrive more than once, and every copy may carry personal data.

**Why It Happens:** RiskOS retries, and Socure warns that it may retry successfully delivered events in rare edge cases. Evaluation events carry the full Evaluation API response.

**Workarounds:**

* Deduplicate on `event_id` before processing.
* Store minimal data from the webhook and fetch details from the RiskOS API when needed, as Socure recommends.

**How Hookdeck Can Help:** Hookdeck [deduplication](/docs/deduplication) can drop repeated deliveries by `event_id` before they reach your service.

## Best practices

### Deduplicate on `event_id`

Socure designates `event_id` as the idempotency key. Store it before you act on an event, and skip anything you have already seen. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Order by `event_at`

A `decision_update` may arrive before the `evaluation_completed` it follows. Compare `event_at`, or `data.updated_at` where present, with what you have stored before changing a decision.

### Log delivery metadata

Record `event_id`, `event_type` and `event_at` for every request, as Socure recommends, so you can trace a decision back to the event that changed it.

### Acknowledge fast and process asynchronously

Respond with `200` well inside 30 seconds and handle the event from a queue. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Use separate webhooks for sandbox and production

The environments have different source IPs and should have different credentials. `data.environment_name` tells you which one sent an event.

## Conclusion

Socure RiskOS sends unsigned webhooks in a standard `event_id`, `event_at`, `event_type`, `data` envelope, authenticated with credentials fixed when the webhook is created. Use Basic Authentication, compare the decoded username and password in constant time over HTTPS, allowlist Socure's IPs, deduplicate on `event_id`, and respond within 30 seconds, remembering that only a handful of status codes are retried.

[Hookdeck Event Gateway](https://hookdeck.com) checks the Basic credentials, keeps every RiskOS event, and retries and replays delivery to your service, so a bug or an outage on your side does not drop an identity decision.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Socure webhooks reliably in minutes.