# Guide to Sohar Health Webhooks: Features and Best Practices

[Sohar Health](https://soharhealth.com) helps behavioural-health providers check a patient's insurance before care starts. Its API verifies eligibility against a payer, discovers a patient's coverage, and runs intake Flows that can pause for more information. Many of those checks don't finish instantly, so Sohar's webhooks tell your application when a verification completes or is still pending, when a discovery request updates, and when a Flow Run changes state or needs input.

## What are Sohar Health webhooks?

Sohar Health delivers webhooks through Svix. You register endpoints and subscribe them to events through Sohar's API, and you find each endpoint's signing secret in the Svix Portal linked from the dashboard.

| Aspect | Value |
| --- | --- |
| Method | `POST`, JSON body over HTTPS |
| Signature headers | `svix-id`, `svix-timestamp`, `svix-signature` (Standard Webhooks) |
| Success | Respond `200 OK` to acknowledge receipt |
| Delivery | No retry schedule, timeout or ordering guarantee is stated in Sohar Health's docs |

Webhooks are managed with production credentials, but any endpoint can also subscribe to sandbox events, which makes testing straightforward.

The Sohar Health skill ships [reference receivers](https://github.com/hookdeck/webhook-skills/tree/main/skills/sohar-health-webhooks/examples) for all ten events.

## Sohar Health webhook features

| Feature | Details |
| --- | --- |
| Configuration | API, `POST /v1/webhooks` then one `POST .../events` per event |
| Environments | Production events and `sandbox.` twins on the same endpoint |
| Signature | Standard Webhooks HMAC-SHA256 |
| Signature header | `svix-signature` |
| Secret | Per endpoint, in the Svix Portal. Format inferred from Svix |
| Body | JSON, flat |
| Event routing | Body field `event` |
| Retries and timeout | Not stated |
| Source IPs | Not published |

## Events and payloads

Sohar documents ten events, five for production and a `sandbox.` twin of each:

| Event | Triggered When | Common Use Cases |
| --- | --- | --- |
| `verification.updated` | A production verification has been completed | Read the result (e.g. `complete.eligible`) and fetch the verification |
| `sandbox.verification.updated` | A sandbox verification has been completed | Testing; the recommended first subscription |
| `verification.pending` | A production verification was not resolved immediately | Show a "checking coverage" state |
| `sandbox.verification.pending` | A sandbox verification was not resolved immediately | Testing |
| `discovery.updated` | A discovery request is updated | Fetch discovered coverage |
| `sandbox.discovery.updated` | A sandbox discovery request is updated | Testing |
| `flow.updated` | The status or state of a Flow Run is updated | Track Flow progress |
| `sandbox.flow.updated` | The status or state of a sandbox Flow Run is updated | Testing |
| `flow.callback` | A Flow Run reaches a callback state and waits for input | Collect and submit the requested input |
| `sandbox.flow.callback` | A sandbox Flow Run reaches a callback state | Testing |

Bodies are notifications. They carry a top-level `event`, the resource's ID and a status, and you fetch the full verification, discovery or Flow Run from the API:

| Event family | Fields (from the docs' examples) |
| --- | --- |
| `verification.updated`, `verification.pending` | `event`, `verificationId`, `id`, `status` (`complete.eligible` / `pending` in the linked skill examples) |
| `discovery.updated` | `event`, `discoveryId`, `status` (`complete`), `id` |
| `flow.updated` | `event`, `flowRunId`, `flowId`, `status` (`COMPLETE`, `ERROR`), `stateId` |
| `flow.callback` | `event`, `flowRunId`, `input` (a JSON Schema describing the input the Flow needs); no `status` |

The `sandbox.` events use the same shapes. Sohar doesn't define the short `id` field beyond showing a value such as `"123456"`, so key on the named resource IDs instead.

This is the `discovery.updated` sample from the [webhook samples library](https://github.com/hookdeck/webhook-samples/blob/main/providers/sohar-health/latest/discovery.updated.json):

```json
{
  "event": "discovery.updated",
  "discoveryId": "fb701ac1-1246-4860-b1ec-bd916b97a990",
  "status": "complete",
  "id": "123456"
}

```

`flow.callback` is different: instead of a status it carries `input`, a JSON Schema describing what the Flow Run needs from you before it can continue.

## Setting up Sohar Health webhooks

Registration takes two kinds of API call against `https://api.soharhealth.com`, each with a bearer token from your production credentials (see Sohar's [authentication](https://docs.soharhealth.com/api-reference/api-reference/authentication) page):

1. **Create Webhook**: `POST /v1/webhooks` with `{"url": "https://example.com/webhooks/sohar-health"}`. The `201` response contains an `endpointId` such as `ep_2pqPuj665SJeVw8DkU3pkZq1njL`.
2. **Create Event**: `POST /v1/webhooks/{endpointId}/events` with `{"event": "sandbox.verification.updated"}` subscribes the endpoint to one event. Repeat it for each event; the response lists the endpoint's `events`.

Sohar recommends `sandbox.verification.updated` as the first subscription for testing.

For the secret, open the endpoint's detail view from the [Webhooks](https://app.soharhealth.com/webhooks) page of your dashboard and choose **Open Svix Portal** from the menu in the top-right corner. Sohar doesn't print a sample secret, but Svix secrets start with `whsec_`; store the whole value:

```bash
SOHAR_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxx

```

To route Sohar Health through Hookdeck, create a source with the **Sohar Health** type, add the secret, and register the source URL with Create Webhook. During development the [Hookdeck CLI](/docs/cli) forwards it to your machine:

```bash
npx hookdeck-cli listen 3000 sohar-health --path /webhooks/sohar-health

```

Run a sandbox verification to send a `sandbox.verification.updated` through it.

## Securing Sohar Health webhooks

Sohar's security page tells you to extract `svix-id`, `svix-timestamp` and `svix-signature` and verify them against the raw body, warning that "even minor changes can invalidate the cryptographic signature". The check is Svix's: HMAC-SHA256 over `<svix-id>.<svix-timestamp>.<raw body>` with the base64-decoded secret after `whsec_`, compared with each `v1,` entry. This function also reads `webhook-*` headers:

```javascript
const crypto = require('node:crypto');

function decodeBase64(value) {
  if (typeof value !== 'string' || !/^[A-Za-z0-9+/]+={0,2}$/.test(value)) return null;
  const bytes = Buffer.from(value, 'base64');
  return bytes.length > 0 && bytes.toString('base64').replace(/=+$/, '') === value.replace(/=+$/, '')
    ? bytes : null;
}

function verifySignature(rawBody, headers, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
  const prefix = headers['svix-id'] ? 'svix' : 'webhook';
  const id = headers[`${prefix}-id`];
  const timestamp = headers[`${prefix}-timestamp`];
  const signature = headers[`${prefix}-signature`];
  if (typeof id !== 'string' || !id || typeof timestamp !== 'string' ||
      !/^[0-9]+$/.test(timestamp) || typeof signature !== 'string') return false;
  const key = decodeBase64(secret.replace(/^whsec_/, ''));
  if (!key) return false;
  const message = Buffer.concat([Buffer.from(`${id}.${timestamp}.`, 'utf8'), rawBody]);
  const candidates = signature.split(/\s+/).filter((item) => item.startsWith('v1,')).map((item) => item.slice(3));
  const expected = crypto.createHmac('sha256', key).update(message).digest();
  return candidates.some((candidate) => {
    const received = decodeBase64(candidate);
    if (!received) return false;
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

```

Sohar sets no timestamp tolerance; the Svix libraries apply five minutes. Behind Hookdeck, which has no tolerance and keeps the original `svix-timestamp` on retries and replays, that window would reject real deliveries, so the function above doesn't use one.

Because each event is only a pointer, the receiver records it and a worker fetches the full result, keeping sandbox events away from production records:

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

// Returns the parsed body when it is a JSON object, otherwise null.
function parseJsonObject(rawBody) {
  try {
    const value = JSON.parse(rawBody.toString('utf8'));
    return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
  } catch {
    return null;
  }
}

// coverageEvents.save(messageId, event, isSandbox) stores the event and resolves once it is durable.
function createSoharReceiver(coverageEvents) {
  const app = express();
  app.post('/webhooks/sohar-health', express.raw({ type: '*/*' }), async (req, res) => {
    const secret = process.env.SOHAR_WEBHOOK_SECRET;
    if (!secret) return res.sendStatus(500);
    if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);

    const event = parseJsonObject(req.body);
    if (!event) return res.sendStatus(400); // signed, but not a JSON object
    if (typeof event.event !== 'string') return res.sendStatus(400);
    const isSandbox = event.event.startsWith('sandbox.');
    try {
      await coverageEvents.save(req.headers['svix-id'] || req.headers['webhook-id'], event, isSandbox);
    } catch {
      return res.sendStatus(503);
    }
    return res.sendStatus(200);
  });
  return app;
}

```

When Sohar sends to a Hookdeck source and the signature is wrong, Hookdeck answers `200`, records the request as failed verification and stops it.

> **Make Sohar Health webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) verifies every Sohar event and can send sandbox and production traffic to different destinations.

## Sohar Health webhook limitations and pain points

### No published delivery policy

**The Problem:** You can't tell whether a verification result missed during an outage will be resent, or for how long.

**Why It Happens:** Sohar only says to respond `200 OK`. It doesn't state a retry schedule, a timeout or an ordering guarantee.

**Workarounds:**

* Respond `200` as soon as the event is stored.
* Poll pending verifications and Flow Runs through the API as a backstop.

**How Hookdeck Can Help:** Hookdeck acknowledges each event immediately and [retries](/docs/retries) your service on a schedule you choose.

### Events are only pointers

**The Problem:** A `verification.updated` doesn't tell you the patient's coverage details.

**Why It Happens:** Bodies carry an ID and a status, not the full result.

**Workarounds:**

* Fetch the verification, discovery or Flow Run from the API after each event.

**How Hookdeck Can Help:** Hookdeck can [rate-limit](/docs/destinations) delivery to the worker that calls Sohar's API, so a burst of events doesn't trigger a burst of API calls.

### Sandbox and production share endpoints

**The Problem:** A sandbox result updates a real patient record.

**Why It Happens:** Any endpoint can receive both production and `sandbox.` events.

**Workarounds:**

* Branch on the `sandbox.` prefix before touching any data, or subscribe separate endpoints to each set.

**How Hookdeck Can Help:** Hookdeck [filters](/docs/filters) on `event` can send `sandbox.*` to a test destination and everything else to production.

### Secret format is inferred

**The Problem:** Verification fails even though you copied a secret.

**Why It Happens:** Sohar never shows a secret's format. Svix secrets start with `whsec_`, and the secret is only available in the Svix Portal for the endpoint that receives the delivery.

**Workarounds:**

* Copy the secret from the Svix Portal of the right endpoint and keep its prefix.

**How Hookdeck Can Help:** Hookdeck's request log flags every failed verification, which shows a wrong secret on the first delivery.

## Best practices

### Answer `flow.callback` promptly

A Flow Run in a callback state is waiting for you. Read the JSON Schema in `input`, collect what it asks for, and submit it so the intake can continue.

### Show a pending state

`verification.pending` means the verification wasn't resolved immediately. Tell the patient or staff that coverage is still being checked, and wait for `verification.updated`.

### Deduplicate on `svix-id` and status

`svix-id` repeats on retries. For resource updates, also deduplicate on the resource ID plus `status`. The [webhook idempotency guide](/webhooks/guides/implement-webhook-idempotency) covers the storage side.

### Protect patient data

Fetched verifications contain health and insurance information. Keep it out of logs and apply your usual safeguards for protected health information.

## Conclusion

Sohar Health's webhooks keep insurance checks and intake moving without polling. Each event is a pointer, so the real integration is the worker that fetches the verification, discovery or Flow Run it names.

[Hookdeck Event Gateway](https://hookdeck.com) separates sandbox from production, retries your service and keeps every Sohar event on record. [Open your Hookdeck account](https://dashboard.hookdeck.com/signup).