Webhook Security
Every webhook delivery for a subscription with awebhook_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 theX-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_secretreturned 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 theX-Webhook-Eventheader to pick the secret of the matching subscription. - There is no separate timestamp header. The
timestampfield in the body is covered by the signature.
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_secretis returned only once: in the response toPOST /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-Eventtells 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. SendPOST /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
Always verify signatures
Always verify signatures
Never process webhook payloads without verifying the signature first. This protects against spoofed requests.
Use constant-time comparison
Use constant-time comparison
Always use
hmac.compare_digest (Python) or crypto.timingSafeEqual (Node.js) to prevent timing attacks.Respond quickly
Respond quickly
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.Process events idempotently
Process events idempotently
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.Reject replayed requests
Reject replayed requests
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.