Skip to main content

Webhooks

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.

How webhooks work

  1. Create a subscription with the URL of your endpoint and the event types you want to receive. Solum returns a signing secret once.
  2. When a matching event occurs, Solum signs the JSON payload with your secret and sends it to your URL with X-Solum-* headers.
  3. Your endpoint verifies the signature, processes the event, and responds 2xx within 30 seconds.
  4. Any non-2xx response or timeout is retried up to two times with exponential backoff (60s, 5m, 15m).

Create a subscription

Create webhook subscriptions through the API or the dashboard at Settings → Webhooks. The example below uses the API. POST https://api.getsolum.com/v1/webhooks Authentication: X-API-Key: <your_api_key>.
curl -X POST https://api.getsolum.com/v1/webhooks \
  -H "X-API-Key: $SOLUM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.example.com/webhooks/solum",
    "event_types": ["patient_created", "patient_verification_completed"],
    "description": "Production endpoint"
  }'
{
  "id": "550e8400-e29b-41d4-a716-446655440016",
  "url": "https://your-app.example.com/webhooks/solum",
  "event_types": ["patient_created", "patient_verification_completed"],
  "is_active": true,
  "secret_key": "whsec_d22f3d511dc64683b40fed32c8425734",
  "description": "Production endpoint",
  "created_at": "2024-01-15T10:30:00Z",
  "updated_at": "2024-01-15T10:30:00Z"
}
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.

Delivery format

Every delivery is an HTTP POST to your endpoint URL with the following headers:
HeaderExampleDescription
Content-Typeapplication/jsonBody is UTF-8 encoded JSON.
User-AgentSolum-Webhooks/1.0Identifies Solum traffic.
X-Solum-Event-Typepatient_verification_completedEvent type, identical to event_type in the body.
X-Solum-Event-ID2805d797-9a0a-4ae0-b969-fba1a3824a50Unique event identifier. Retries reuse the same ID; use it to deduplicate.
X-Solum-Signaturesha256=8474e2aa67164a7fdb0f68017582eb141a705b7058c29613d5b8d8ae7fb29c7dHMAC-SHA256 of the raw body, hex-encoded. See Verifying signatures.
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.

Event types

Event typeWhen it fires
patient_createdA new patient is created (any source — API, dashboard, batch import).
patient_updatedAn existing patient’s fields change. Body lists the changed field names (not values).
patient_stage_changedA patient moves to a different workflow stage.
patient_insurance_verificationA patient enters the Insurance Verification stage (created in it or moved into it).
patient_verification_completedAll verifications for a patient are completed (via the Complete Verification button or a stage action).
task_createdA new task is created.
task_assignedA task’s assignee list changes after creation.
task_completedA task transitions to status: completed.
task_updatedCatch-all for other task field changes (priority, deadline, title, description, tags, non-completion status).
task_comment_createdA comment is added to a task.

Example payloads

All payloads share three top-level fields: event_type, event_id, timestamp. The rest depends on the event.
{
  "event_type": "patient_created",
  "event_id": "2805d797-9a0a-4ae0-b969-fba1a3824a50",
  "timestamp": "2024-01-15T10:30:00.123456",
  "patient": { "id": "9c1a3b8f-..." },
  "created_by": "u_5a2c..."
}
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.

Verifying signatures

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:
X-Solum-Signature: sha256=8474e2aa67164a7fdb0f68017582eb141a705b7058c29613d5b8d8ae7fb29c7d
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.

Verify signatures manually

Step 1: Read the raw request body

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.

Step 2: Prepare the signing key

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.

Step 3: Compute the expected signature

Compute an HMAC using SHA-256 with the cleaned secret as the key and the raw body as the message. Hex-encode the digest.

Step 4: Compare against the header

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

Code example

import hashlib
import hmac

from fastapi import FastAPI, Request, HTTPException

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

Reference test vector

Use this fixed vector to confirm your implementation produces the expected hex before connecting to live deliveries.
InputValue
Signing secretwhsec_abc123
Request body{"test":true}
Expected X-Solum-Signaturesha256=d6973f7d4441d3e3af74c4839f744987c2684a1b097ddca0cb4f936a64d66fad
Reproduce the expected hex in any of the following environments — each snippet is fully self-contained and prints d6973f7d4441d3e3af74c4839f744987c2684a1b097ddca0cb4f936a64d66fad.
printf '%s' '{"test":true}' | openssl dgst -sha256 -hmac "abc123" -hex

Verify a specific delivery

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:
SECRET="d22f3d511dc64683b40fed32c8425734"   # whsec_ prefix removed
openssl dgst -sha256 -hmac "$SECRET" -hex < received_body.bin
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.

Test events

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.
curl -X POST https://api.getsolum.com/v1/webhooks/{subscription_id}/tests \
  -H "X-API-Key: $SOLUM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "event_type": "patient_verification_completed" }'
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.

Retries

ParameterValue
Timeout per attempt30 seconds
Maximum attempts3 (initial delivery plus two retries)
Backoff60s, 5m, 15m
Retry triggerAny 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.

Idempotency

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.

Troubleshooting

SymptomCauseResolution
All signatures failThe whsec_ prefix is part of the HMAC key.Strip the prefix before computing the HMAC.
All signatures failThe secret is hex- or base64-decoded before HMAC.Use the characters after whsec_ directly as UTF-8 bytes.
Most signatures failThe 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 payloadsReceiver is decoding the body with a non-UTF-8 encoding.Read the body as UTF-8 bytes.
Test deliveries succeed; production failsA 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 eventsA retry succeeded after the original request exceeded the 30-second timeout.Deduplicate on X-Solum-Event-ID.
Solum logs 401 for your endpointThe 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.