Gareth Wilson Gareth Wilson

Guide to Sohar Health Webhooks: Features and Best Practices

Published


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.

AspectValue
MethodPOST, JSON body over HTTPS
Signature headerssvix-id, svix-timestamp, svix-signature (Standard Webhooks)
SuccessRespond 200 OK to acknowledge receipt
DeliveryNo 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

FeatureDetails
ConfigurationAPI, POST /v1/webhooks then one POST .../events per event
EnvironmentsProduction events and sandbox. twins on the same endpoint
SignatureStandard Webhooks HMAC-SHA256
Signature headersvix-signature
SecretPer endpoint, in the Svix Portal. Format inferred from Svix
BodyJSON, flat
Event routingBody field event
Retries and timeoutNot stated
Source IPsNot published

Events and payloads

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

EventTriggered WhenCommon Use Cases
verification.updatedA production verification has been completedRead the result (e.g. complete.eligible) and fetch the verification
sandbox.verification.updatedA sandbox verification has been completedTesting; the recommended first subscription
verification.pendingA production verification was not resolved immediatelyShow a "checking coverage" state
sandbox.verification.pendingA sandbox verification was not resolved immediatelyTesting
discovery.updatedA discovery request is updatedFetch discovered coverage
sandbox.discovery.updatedA sandbox discovery request is updatedTesting
flow.updatedThe status or state of a Flow Run is updatedTrack Flow progress
sandbox.flow.updatedThe status or state of a sandbox Flow Run is updatedTesting
flow.callbackA Flow Run reaches a callback state and waits for inputCollect and submit the requested input
sandbox.flow.callbackA sandbox Flow Run reaches a callback stateTesting

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 familyFields (from the docs' examples)
verification.updated, verification.pendingevent, verificationId, id, status (complete.eligible / pending in the linked skill examples)
discovery.updatedevent, discoveryId, status (complete), id
flow.updatedevent, flowRunId, flowId, status (COMPLETE, ERROR), stateId
flow.callbackevent, 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):

  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 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 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 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.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.