Solum sends webhook events to URLs you register when patients and tasks in your workspace change — created, updated, stage changed, verification completed, comment added. Each delivery is signed with HMAC-SHA256 so you can confirm it came from Solum and that the body has not been modified in transit.
Webhook payloads are intentionally ID-only and do not contain PHI, following the same convention as Epic and athenahealth. The event tells you what changed and which record changed; fetch the current state from the corresponding GET /v1/... endpoint with your API key.
Create webhook subscriptions through the API or the dashboard at Settings → Webhooks. The example below uses the API.POST https://api.getsolum.com/v1/webhooksAuthentication: X-API-Key: <your_api_key>.
The signing secret is returned only on creation. Store it in a secrets manager — it cannot be retrieved later. If the secret is lost, delete the subscription and create a new one.
Subscription management endpoints (list, update, delete, test, list deliveries, redeliver) are documented in the API Reference.
The body is compact JSON with no whitespace. Sign the raw bytes you received, not a re-serialized form (see Verifying signatures).Your endpoint must return a 2xx status within 30 seconds. Any other response, or a timeout, is treated as a failure and retried.
Payloads never contain PHI such as names, phone numbers, email addresses, or dates of birth. Fetch the full record from GET /v1/patients/{id} or GET /v1/tasks/{id} using the ID in the payload.
Solum signs every delivery with HMAC-SHA256 using your subscription’s signing secret. Verify the signature on every incoming request to confirm it came from Solum and that the body has not been modified in transit.The X-Solum-Signature header contains a single scheme — sha256 — followed by the hex-encoded digest:
Solum does not yet ship official client libraries, so verification is implemented in your own code. The steps below describe the algorithm; complete code examples for Python and TypeScript follow.
The HMAC input is the raw bytes of the HTTP request body, byte-for-byte. If your framework parses the body into a dict or object and you re-serialize it, key order, whitespace, and unicode escaping can differ from the original — and the signature will not match.Read the body before any parsing. In FastAPI: await request.body(). In Flask: request.get_data(). In Django: request.body. In Express: mount express.raw({ type: "application/json" }) on the webhook route, ahead of any express.json() middleware.
Your subscription secret has the form whsec_<hex>. The whsec_ prefix is a label identifying the secret’s purpose — it is not part of the cryptographic key. Strip it before computing the HMAC.The remaining characters are used directly as UTF-8 bytes. There is no hex-decoding or base64-decoding step.
Take everything after sha256= in X-Solum-Signature and compare it to your computed hex using a constant-time comparison (hmac.compare_digest in Python, crypto.timingSafeEqual in Node).
import hashlibimport hmacfrom fastapi import FastAPI, Request, HTTPExceptionapp = FastAPI()SOLUM_SIGNING_SECRET = "whsec_d22f3d511dc64683b40fed32c8425734"def verify_solum_signature(raw_body: bytes, header_value: str, secret: str) -> bool: if not header_value or not header_value.startswith("sha256="): return False received = header_value.split("=", 1)[1] clean_secret = secret.removeprefix("whsec_") expected = hmac.new( clean_secret.encode("utf-8"), raw_body, hashlib.sha256, ).hexdigest() return hmac.compare_digest(received, expected)@app.post("/webhooks/solum")async def solum_webhook(request: Request): raw = await request.body() sig = request.headers.get("x-solum-signature", "") if not verify_solum_signature(raw, sig, SOLUM_SIGNING_SECRET): raise HTTPException(status_code=401, detail="Invalid signature") event = await request.json() # handle event["event_type"] return {"received": True}
In Express, mount express.json() after your webhook route or scope it to non-webhook paths. If it runs first it will consume the raw body and your HMAC will never match.
Reproduce the expected hex in any of the following environments — each snippet is fully self-contained and prints d6973f7d4441d3e3af74c4839f744987c2684a1b097ddca0cb4f936a64d66fad.
When verification fails on a live request, save the exact bytes Solum POSTed to a file — for example by capturing them in your access log — and re-run the HMAC outside your application:
The hex output must match the value after sha256= in the X-Solum-Signature header. If it doesn’t, the cause is almost always one of:
The whsec_ prefix was kept as part of the HMAC key.
The body was modified after receipt — a logger appended a newline, a reverse proxy re-encoded the JSON, or your handler parsed and re-serialized it before signing.
The secret was rotated and the request was signed with the previous value.
Each subscription exposes a test endpoint that delivers a synthetic event to your URL using the subscription’s real signing secret. Use it to validate end-to-end signature verification before enabling production traffic.
Test events use placeholder identifiers (test-patient-id, test-user-id) and an event_id prefixed with test_. The same action is available in the dashboard under Settings → Webhooks → Test.
Any non-2xx response, timeout, or connection error
Every retry reuses the same event_id. After three failed attempts the delivery is marked failed. To redeliver any delivery — including successful ones — call:
curl -X POST https://api.getsolum.com/v1/webhooks/{subscription_id}/deliveries/{delivery_id}/attempts \ -H "X-API-Key: $SOLUM_API_KEY"
List recent delivery attempts with GET /v1/webhooks/{subscription_id}/deliveries.
Webhook delivery is at-least-once. A network delay can cause Solum to retry a request your handler already processed, because the original 2xx response did not arrive within the timeout.Persist the X-Solum-Event-ID of every event you process and short-circuit duplicates on subsequent attempts. The header and the event_id field in the body always hold the same value.
Use the characters after whsec_ directly as UTF-8 bytes.
Most signatures fail
The body is parsed and re-serialized before HMAC.
Sign the raw request body. Use await request.body() in FastAPI, request.get_data() in Flask, express.raw({ type: "application/json" }) in Express.
Signatures fail only on non-ASCII payloads
Receiver is decoding the body with a non-UTF-8 encoding.
Read the body as UTF-8 bytes.
Test deliveries succeed; production fails
A proxy in front of the production endpoint mutates the body (re-encoding, compression, character rewrites).
Ensure the handler sees byte-identical input to what Solum sent.
Duplicate events
A retry succeeded after the original request exceeded the 30-second timeout.
Deduplicate on X-Solum-Event-ID.
Solum logs 401 for your endpoint
The endpoint URL returns a redirect or auth challenge before the request reaches your handler.
Expose the webhook URL without an auth gate and verify the HMAC inside the handler.
For unresolved issues, contact support@getsolum.com with the delivery ID from GET /v1/webhooks/{subscription_id}/deliveries and the hex your implementation computes.