Guide to Sohar Health Webhooks: Features and Best Practices
Sohar Health 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 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:
{
"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 page):
- Create Webhook:
POST /v1/webhookswith{"url": "https://example.com/webhooks/sohar-health"}. The201response contains anendpointIdsuch asep_.2pqPuj665SJeVw8DkU3pkZq1njL - Create Event:
POST /v1/webhooks/{endpointId}/eventswith{"event": "sandbox.verification.updated"}subscribes the endpoint to one event. Repeat it for each event; the response lists the endpoint'sevents.
Sohar recommends sandbox.verification.updated as the first subscription for testing.
For the secret, open the endpoint's detail view from the 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:
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 forwards it to your machine:
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:
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:
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 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
200as 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 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 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 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 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 separates sandbox from production, retries your service and keeps every Sohar event on record. Open your Hookdeck account.