Webhooks
Webhooks let RCMS push events to your system in real time — client enrolled, assessment submitted, recovery plan updated, staff deactivated. Subscribe once; RCMS calls your endpoint whenever a relevant event happens.
When to use webhooks (and when not)
| Use case | Use |
|---|---|
| React to a client being enrolled or discharged | Webhook |
| Sync assessment scores into your system as they're finalized | Webhook |
| Trigger a notification when an assessment becomes overdue | Webhook |
| Look up a client's latest score on demand | JSON API |
| Periodic batch sync (nightly refresh, backfills) | JSON API |
Webhooks are for push notifications of state changes. For on-demand reads, use the API.
Register a webhook
curl -X POST https://api.measurerecovery.com/v1/webhooks \
-H "Authorization: Bearer rcms_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"url": "https://partner.example.com/hooks/rcms",
"events": [
"assessment.submitted",
"assessment.scored",
"client.enrolled",
"client.discharged"
],
"description": "Production webhook for OneStep integration"
}'Response:
{
"data": {
"id": "wh_01J8XR7K9M1N2P3Q4R5S6T7U8",
"url": "https://partner.example.com/hooks/rcms",
"events": ["assessment.submitted", "assessment.scored", "client.enrolled", "client.discharged"],
"description": "Production webhook for OneStep integration",
"signing_secret": "whsec_rK2nF9aL7pM4xQ8vR5jS3wN6yB1zT0uH",
"status": "active",
"created_at": "2026-04-19T14:30:00Z"
}
}Save the signing_secret now.
It's shown once at creation and never again. You'll need it to verify every incoming webhook signature. If lost, delete the webhook and register a new one.
Event types
Subscribe to only the events you need; ignore everything else. Specifying more events means more noise on your endpoint.
| Event type | Fires when |
|---|---|
| client.enrolled | New client record created |
| client.updated | Client profile fields change |
| client.discharged | Client marked as discharged |
| client.readmitted | Previously discharged client re-enters the program |
| client.intake_incomplete | Client created with required intake fields still pending |
| assessment.scheduled | Next assessment due date set |
| assessment.submitted | Client or staff submits an assessment (scoring pending) |
| assessment.scored | Scoring finished; domain scores available |
| assessment.locked | Assessment edit window closed; results immutable |
| assessment.due_soon | Assessment approaching its due date (7 days out) |
| assessment.overdue | Assessment past its due date, not yet submitted |
| staff.created | New staff member provisioned |
| staff.updated | Staff role or permissions change |
| staff.deactivated | Staff account disabled |
| organization.created | New org provisioned within a network |
| organization.updated | Org profile fields change |
Event payload format
Every delivery is a POST to your URL with a consistent envelope. The data field shape varies by event type.
POST /hooks/rcms HTTP/1.1
Host: partner.example.com
Content-Type: application/json
X-RCMS-Signature: t=1713546600,v1=5257a869e7ecfe04b08e4107d4e9c2d3b1c7e9f0a8e1...
X-RCMS-Event-Id: evt_01J8XS9P2Q3R4S5T6U7V8W9X0Y
User-Agent: RCMS-Webhooks/1.0
{
"id": "evt_01J8XS9P2Q3R4S5T6U7V8W9X0Y",
"type": "assessment.scored",
"created_at": "2026-04-19T14:33:21Z",
"organization_id": "org_8fKzW2",
"data": {
"assessment_id": "asmt_01J8WR6P5N4M3L2K1J",
"client_id": "client_01J8W2K3L4M5N6P",
"assessment_number": 3,
"submitted_at": "2026-04-19T14:33:10Z",
"positive_score": 61.8,
"negative_score": -27.4,
"overall_score": 17.2,
"engagement_score": 43
}
}Envelope fields
| Field | Description |
|---|---|
| id | Unique ID for this event. Use it for idempotency (de-duplication on retries). |
| type | Event type (see list above) |
| created_at | ISO-8601 timestamp of when the event occurred |
| organization_id | Which org this event belongs to |
| data | Event-specific payload. Shape depends on type. |
Verify the signature
Every delivery carries an X-RCMS-Signature header in this format:
X-RCMS-Signature: t=1713546600,v1=5257a869e7ecfe04b08e4107d4e9c2d3b1c7e9f0a8e1...t— Unix timestamp of when the signature was generatedv1— HMAC-SHA256 hex digest of{t}.{raw_body}, using your webhook's signing secret
Verification steps
- Parse
X-RCMS-Signatureintotandv1 - Compute the expected HMAC-SHA256 of
{t}.{raw_body}using your signing secret - Compare to
v1with a constant-time comparison - Reject if the timestamp is older than 5 minutes (replay protection)
Node.js example
import crypto from "node:crypto";
function verifyRcmsWebhook(rawBody, signatureHeader, secret) {
const parts = Object.fromEntries(
signatureHeader.split(",").map((kv) => kv.split("="))
);
const timestamp = parseInt(parts.t, 10);
const signature = parts.v1;
// Reject replays older than 5 minutes
if (Math.abs(Date.now() / 1000 - timestamp) > 300) {
throw new Error("Webhook timestamp too old");
}
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
throw new Error("Webhook signature invalid");
}
}Python example
import hmac
import hashlib
import time
def verify_rcms_webhook(raw_body: bytes, signature_header: str, secret: str) -> None:
parts = dict(kv.split("=") for kv in signature_header.split(","))
timestamp = int(parts["t"])
signature = parts["v1"]
if abs(time.time() - timestamp) > 300:
raise ValueError("Webhook timestamp too old")
expected = hmac.new(
secret.encode(),
f"{timestamp}.{raw_body.decode()}".encode(),
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(expected, signature):
raise ValueError("Webhook signature invalid")Notification routing — who sends what to users
RCMS sends a few transactional messages natively (assessment reminders, follow-up nudges). When a partner integration is involved, clients often end up signed up through the partner's platform — at that point RCMS emails from an unfamiliar domain create confusion.
To avoid that, notification routing follows the auth identity owner rule. No org-level configuration; no duplicate emails:
| Who created the auth identity | How reminder notifications are delivered |
|---|---|
| Your API key (partner-provisioned user) | RCMS fires assessment.due_soon / assessment.overdue webhooks to your endpoint. RCMS does not send its own email. |
| RCMS (org admin invited the user directly) | RCMS sends its native reminder email from noreply@measurerecovery.com. No webhook is fired for reminders. |
If you subscribe to assessment.due_soon and assessment.overdue, you'll receive events for every client your API key created — RCMS assumes you're handling the outreach from your domain with your branding. Clients you didn't create stay on RCMS-native email delivery automatically.
Applies to reminder events only
Transactional events you want for analytics or sync (assessment.scored, client.discharged, etc.) fire for every subscribed webhook regardless of who created the auth identity. This routing rule only affects user-facing notifications where duplicate branding would cause confusion.
Delivery semantics
| Behavior | Detail |
|---|---|
| At-least-once delivery | You may receive the same event more than once. Use the id to deduplicate. |
| Success criteria | Return 2xx within 30 seconds |
| Retry trigger | Non-2xx response, timeout, or connection error |
| Retry schedule | Exponential backoff: 30s, 2m, 10m, 30m, 1h, 2h, 6h, 12h — total window 24 hours |
| Dead letter | After 24 hours of failures, delivery is marked failed. Event stays queryable via GET /v1/webhooks/:id/deliveries. |
| Ordering | Not guaranteed. Use created_at to order. |
| Disabled endpoints | After 50 consecutive failures, the webhook auto-disables. Re-enable by deleting and re-registering. |
Test, inspect, and manage webhooks
Send a test event
curl -X POST https://api.measurerecovery.com/v1/webhooks/wh_01J8XR.../test \
-H "Authorization: Bearer rcms_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"Sends a synthetic webhook.test event to your URL so you can verify your endpoint accepts and signs correctly.
List recent deliveries
curl https://api.measurerecovery.com/v1/webhooks/wh_01J8XR.../deliveries \
-H "Authorization: Bearer rcms_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"Returns the last 100 deliveries with status, response code, attempt count, and timestamps. Useful for debugging partner-side issues.
Remove a webhook
curl -X DELETE https://api.measurerecovery.com/v1/webhooks/wh_01J8XR... \
-H "Authorization: Bearer rcms_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"Best practices
- Return 2xx immediately, then process async.Don't block the webhook response on slow downstream work. Queue the event and return 200, then process in a background worker.
- Deduplicate on event
id. Store processed event IDs (for ~48h) and skip if seen before. Retries are expected. - Always verify the signature. Reject any request with a missing or invalid
X-RCMS-Signatureheader. Don't trust the body alone. - Use HTTPS with a valid certificate. HTTP endpoints are rejected at registration time.
- Subscribe narrowly.Only events you'll actually act on — ignoring events wastes bandwidth and muddies your logs.
- Monitor delivery failures. Poll
/deliveriesor set up an alert for HTTP 500s on your endpoint. Silent breakage means silent data drift. - Keep the signing secret in a secrets manager. Never commit it, never log it, never embed it in a browser build.