Webhooks
Webhooks let RCMS push assessment events to your system as they happen: an assessment submitted, scored or locked, or one coming due. RCMS registers your endpoint during onboarding and then calls it whenever a subscribed event occurs.
When to use webhooks (and when not)
| Use case | Use |
|---|---|
| Sync assessment scores into your system as they are finalized | Webhook |
| Trigger a notification when an assessment is coming due or is overdue | Webhook |
| Look up a client's latest score on demand | JSON API |
| Periodic batch sync (nightly refresh, backfills) | JSON API |
| React to a client being enrolled, discharged or readmitted | JSON API (delta sync with updated_after). Client events are not yet emitted; see below. |
Webhooks are for push notifications of state changes. For on-demand reads, use the API.
Registration
RCMS support registers your webhook during partner onboarding. Send us:
- the endpoint URL (must be
https://), - the events you want (from the list below),
- a short description (for example, "Production sync for our case management system").
You receive the webhook id (a UUID) and a signing secret, delivered once through a secure channel, never by email. RCMS does not display the secret again; if it is lost, ask support to rotate it. Changes to the URL or the event list also go through support.
No self-serve registration endpoint yet
The POST /v1/webhooks operation in the API reference is marked Status: roadmap and is not callable; the gateway returns 404 for it. There is no endpoint to send test events, list deliveries or delete a webhook. The delivery contract on this page is what registered endpoints receive today.
Event types
Emitted today
| Event type | Fires when |
|---|---|
| assessment.submitted | A client or staff member submits an assessment (scoring may still be pending) |
| assessment.scored | Scoring finished; the scores are in the payload |
| assessment.locked | The edit window closed; the assessment is immutable |
| assessment.due_soon | Daily sweep: a scheduled assessment is within 7 days of its due date. Fires again if the assessment is rescheduled into the window; deduplicated per session and due date |
| assessment.overdue | Daily sweep: a scheduled assessment is past its due date and not yet submitted |
Changed 2026-09-16: assessment.due_soon now fires earlier, and can fire twice
- The notice window widens from 3 days to 7 days before the due date, so you receive each reminder earlier and
days_until_duecan now be any value from 1 to 7. - If an assessment is rescheduled into the window, it fires again for the new due date. Previously an assessment produced at most one
assessment.due_soonevent, ever. - The change took effect at 19:33 UTC on 2026-09-16. The reminder sweep runs once a day at 09:00 UTC, so the first reminders under the 7-day window are generated by the sweep at 09:00 UTC on 2026-09-17. No new reminders are generated between those two times, because the sweep does not run; retries of earlier reminders continue on their normal schedule.
- Deliveries are deduplicated per session and due date, so a session whose due date does not change is still announced once. This is how RCMS schedules the sweep; it is not a delivery guarantee. Webhooks remain at-least-once, and you should keep discarding repeats by
event_idas described below.
Nothing changes on your endpoint, your subscription or the payload shape. If you treat assessment.due_soon as “once per assessment”, that assumption no longer holds: key your own reminder state on session_id together with due_date.
Accepted at registration, not yet emitted
These names are valid in the event list and can be included in a registration, but RCMS does not deliver them today. No delivery date is committed. When one goes live, its payload shape is added to this page and support tells subscribed partners; nothing changes on your URL.
client.enrolled, client.updated, client.discharged, client.readmitted, client.intake_incomplete, assessment.scheduled, staff.created, staff.updated, staff.deactivated, organization.created, organization.updated
Event payload format
Every delivery is a POST to your URL with the same envelope. The body is compact JSON (no whitespace); the data field varies by event type.
POST /hooks/rcms HTTP/1.1
Host: partner.example.com
Content-Type: application/json
User-Agent: RCMS-Webhook/v1
X-Webhook-Signature: sha256=5257a869e7ecfe04b08e4107d4e9c2d3b1c7e9f0a8e1b2c3d4e5f60718293a4b
X-Webhook-Event-Id: 3f0c9a4e-7d2b-4c8e-9a1f-5b6d7e8f9a0b
X-Webhook-Event-Type: assessment.scored
X-Webhook-Delivery-Attempt: 1
{
"id": "evt_3f0c9a4e-7d2b-4c8e-9a1f-5b6d7e8f9a0b",
"type": "assessment.scored",
"created_at": "2026-09-10T14:33:21.512Z",
"organization_id": "0f8e7d6c-5b4a-4c3d-8e2f-1a0b9c8d7e6f",
"data": {
"object": "assessment_session",
"session_id": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
"client_id": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
"assessment_number": 3,
"due_date": "2026-09-10",
"submitted_at": "2026-09-10T14:33:10Z",
"scored_at": "2026-09-10T14:33:21Z",
"scores": {
"total_score": 72.5,
"total_max_score": 120,
"total_percentage": 60.4,
"overall_recovery_capital": 18.0,
"negative_capital": 42.0,
"positive_capital": 60.4
},
"barriers_count": 3,
"strengths_count": 9,
"next_due_date": "2026-12-09"
}
}The example body is shown pretty-printed for readability; the bytes on the wire are compact.
Headers
| Header | Value |
|---|---|
| X-Webhook-Signature | sha256=<hex>: HMAC-SHA256 of the exact request body, keyed with your signing secret |
| X-Webhook-Event-Id | UUID of the event. Retries carry the same id, so use it to deduplicate. This is the event id the API Terms of Service refer to (section 6.1); the envelope id carries the same UUID with an evt_ prefix. |
| X-Webhook-Event-Type | The event type, for example assessment.scored |
| X-Webhook-Delivery-Attempt | 1 on the first attempt, then 2 to 7 on retries |
| User-Agent | RCMS-Webhook/v1 |
Envelope fields
| Field | Description |
|---|---|
| id | evt_ followed by the same UUID as X-Webhook-Event-Id. Use either for idempotency. |
| type | Event type (see the list above) |
| created_at | ISO-8601 UTC timestamp of when the event was raised |
| organization_id | The organization's public UUID; the same value you send as X-Organization-Id on API calls |
| data | Event-specific payload. Every id inside data is a plain UUID. |
The data object by event type
assessment.submitted and assessment.locked:
{
"object": "assessment_session",
"session_id": "<uuid>",
"client_id": "<uuid, the client_uuid the API returns>",
"assessment_number": 3,
"due_date": "2026-09-10",
"submitted_at": "2026-09-10T14:33:10Z",
"locked_at": null
}assessment.scored: the fields above (without locked_at) plusscored_at, scores, barriers_count and strengths_count, as in the full example. The score to display, if you show one, is scores.overall_recovery_capital: a points value from -100 to +100. total_percentage is an internal figure, not a resident-facing number. next_due_date is the due date of the assessment RCMS scheduled next for this client, or null when none was scheduled, so you do not need a follow-up status call to learn it. Since API 1.4.0 assessment.scored is emitted once per distinct result: a rescoring run that produces the same scores and the same submitted_at is not sent again; a changed score or a resubmission arrives as a new event with a new id.
assessment.due_soon and assessment.overdue:
{
"object": "assessment_session",
"session_id": "<uuid>",
"client_id": "<uuid>",
"assessment_number": 3,
"due_date": "2026-09-17",
"days_until_due": 7
}assessment.overdue has the same fields with days_overdue (a positive integer) in place of days_until_due.
For assessment.due_soon, days_until_due is between 1 and 7 from 2026-09-16. If the assessment is rescheduled, a second event carries the new due_date and a days_until_due counted from it, so the pair (session_id, due_date) identifies a reminder.
Verify the signature
Every delivery carries an X-Webhook-Signature header in this format:
X-Webhook-Signature: sha256=5257a869e7ecfe04b08e4107d4e9c2d3b1c7e9f0a8e1b2c3d4e5f60718293a4bThe value after sha256= is the lowercase hex HMAC-SHA256 digest of the raw request body, keyed with your signing secret. There is no timestamp in the signature and no replay window.
Verification steps
- Read the request body as raw bytes, before any JSON parsing.
- Strip the
sha256=prefix from the header value. - Compute HMAC-SHA256 of the raw bytes with your signing secret and encode it as lowercase hex.
- Compare the two with a constant-time comparison; reject the request if they differ.
- Only then parse the JSON body.
Hash the bytes exactly as received. Re-serializing a parsed object changes key order or whitespace and the digest will not match.
Node.js example
import crypto from "node:crypto";
// rawBody: the request body exactly as received (Buffer or string), before JSON.parse
export function verifyRcmsWebhook(rawBody, signatureHeader, secret) {
// header looks like: sha256=5257a869e7ec...
if (!signatureHeader || !signatureHeader.startsWith("sha256=")) return false;
const received = signatureHeader.slice("sha256=".length);
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
if (received.length !== expected.length) return false;
return crypto.timingSafeEqual(Buffer.from(received, "hex"), Buffer.from(expected, "hex"));
}Python example
import hmac
import hashlib
# raw_body: the request body bytes exactly as received, before json.loads
def verify_rcms_webhook(raw_body: bytes, signature_header: str, secret: str) -> bool:
# header looks like: sha256=5257a869e7ec...
header = signature_header or ""
received = header[7:] if header.startswith("sha256=") else ""
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(received, expected)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.submitted, assessment.scored, assessment.locked) 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. Deduplicate on X-Webhook-Event-Id (or the envelope id). |
| Success criteria | Return any 2xx within 30 seconds, the deadline in the API Terms of Service (section 6.4). See the note below on the current timeout. |
| Retry trigger | Non-2xx response, timeout or connection error |
| Retry schedule | 1 minute, 5 minutes, 15 minutes, 1 hour, 4 hours, 12 hours, 24 hours after each failure: 7 attempts in total, about 18.5 hours end to end |
| After the last attempt | The delivery is marked failed and is not retried automatically. Contact support if you need it re-sent. |
| Ordering | Not guaranteed. Use created_at to order. |
Note on the current timeout: the dispatcher today waits 10 seconds for your response before treating the delivery as failed, and that wait is being aligned to the 30-second deadline in the Terms. Until then a response slower than 10 seconds may be retried. Retries carry the same event id, so a deduplicating receiver is unaffected; if you see repeated deliveries to a slow endpoint, tell support.
Testing and support
There is no self-serve test endpoint yet. To exercise your receiver before real traffic:
- Verify your signature code locally. Build a delivery the way RCMS does: take the compact JSON envelope as a byte string, compute HMAC-SHA256 with your secret, prefix the hex with
sha256=, and post it to your endpoint. Your code should accept it and reject the same body with one byte changed. - Ask support for a sandbox delivery. Register the same URL against your sandbox API key and support can trigger an assessment event there.
- Delivery history. Support can look up any delivery by its event id, including attempt count and your endpoint's last response code.
Best practices
- Return 2xx quickly, then process async. The Terms give you 30 seconds, but do not use them. Queue the event and return 200, then process in a background worker.
- Deduplicate on the event id. Store processed ids (for a few days) and skip repeats. Retries are expected and carry the same id.
- Always verify the signature. Reject any request with a missing or invalid
X-Webhook-Signatureheader. Do not trust the body alone. - Use HTTPS with a valid certificate. Registration requires an
https://URL. - Subscribe narrowly. Only the events you will act on.
- Monitor your endpoint. Alert on 5xx responses and on timeouts from your own side; 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.