Skip to main content

Webhook Security

Every webhook delivery for a subscription with a webhook_secret includes a signature that you should verify to ensure the request originated from RxScale and was not tampered with.

Signature Verification

Each webhook request carries its signature in the X-Webhook-Signature header:
  • The signature is the HMAC-SHA256 of the raw request body, exactly as received, encoded as lowercase hexadecimal (64 characters). There is no prefix such as sha256=.
  • The key is the webhook_secret returned when you registered the subscription, used as a UTF-8 string exactly as returned. Do not decode it.
  • Each subscription (target URL, event type and pharmacy scope) has its own webhook_secret. If you receive several event types at the same endpoint, use the X-Webhook-Event header to pick the secret of the matching subscription.
  • There is no separate timestamp header. The timestamp field in the body is covered by the signature.
Compute the HMAC over the raw bytes before you parse the JSON. If you parse the body and serialise it again, the bytes change and the signature no longer matches.

Example (Python)

Example (Node.js)

Test webhooks carry the header X-Webhook-Test: true so you can tell them apart from real deliveries. Those sent from the Test Webhook card in the Pharmacy Portal are not signed. Those sent through the Management API are signed like a real delivery when an active subscription matches the target URL and event type, and unsigned when none does.

Webhook Secret

  • The webhook_secret is returned only once: in the response to POST /v1/external_pharmacy_api/webhooks/, or in the Pharmacy Portal right after you register a webhook. It is stored encrypted and cannot be retrieved later — GET /v1/external_pharmacy_api/webhooks/ never returns it.
  • Each subscription has its own secret. X-Webhook-Event tells you the event type of a delivery, but nothing identifies the subscription itself, so subscriptions that share an event type — for example one per pharmacy of a group — cannot be told apart. Give those a separate target URL each (for example a different path or query parameter), so you always know which secret to verify with.
  • The value of a custom header (header_value) is never returned either; only its name (header_key) is.

Rotating the Secret

There is no dedicated rotation endpoint. You have two options: Register the same subscription again. Send POST /v1/external_pharmacy_api/webhooks/ again with the same notification_type, target, and pharmacy_uid. RxScale keeps the subscription’s uid and returns a new webhook_secret, which takes effect immediately — the old secret stops working. Every delivery is signed when it is sent, so all deliveries after this request, including retries of earlier events, use the new secret. Store it right away. The request also replaces the custom header: if you omit header_key and header_value, the custom header is removed. Rotate with an overlap. Register a second subscription for the same event with a different target URL, and verify the deliveries to that URL with the new subscription’s secret. Then delete the old subscription with DELETE /v1/external_pharmacy_api/webhooks/{subscription_uid}. While both subscriptions exist, every event is delivered to both, each signed with its own subscription’s secret. See Webhooks (External Pharmacy API) for the endpoint details.

Best Practices

Never process webhook payloads without verifying the signature first. This protects against spoofed requests.
Always use hmac.compare_digest (Python) or crypto.timingSafeEqual (Node.js) to prevent timing attacks.
You must return a 2xx response within the delivery timeout of 30 seconds. A slower response (or no response) counts as a failed delivery and is retried with exponential backoff — see the Retry Policy.We nonetheless recommend acknowledging much faster than that — a good target is under 5 seconds — by returning 2xx as soon as you have stored the payload and doing any heavy processing asynchronously afterwards. The 5 seconds is a recommendation, not a requirement; only the 30-second timeout is enforced.
The same state can be delivered more than once, and deliveries can arrive out of order. Key your processing on data.uid and treat every payload as the latest full state. Do not deduplicate on timestamp — it changes with every delivery attempt. See Processing Events Idempotently.
The timestamp in the body is set when a delivery attempt is sent and is covered by the signature. To limit the replay of an intercepted request, you can reject deliveries whose timestamp differs from your server time by more than a few minutes. Retries are not affected, because every attempt carries a new timestamp.