Overview
Subscribe an HTTPS endpoint to your Rilla organization and we'll POST a JSON event to it every time a conversation is fully processed (transcript, summary, scoring, and surveys all available). Use webhooks to push conversation insights into your data warehouse, CRM, or downstream automations as soon as they're ready, instead of polling the export API.
Event
event_type: conversation_processedSubscribing
How to register your endpoint with Rilla.
Custom headers
Send authentication or routing headers with your events.
Payload
Shape of the JSON event we send.
Verifying signatures
Authenticate that a request came from Rilla.
Retries & delivery
What we do when your endpoint is slow or down.
Code Examples
Copy-paste handlers in Python, Node.js, and cURL.
How it works
As an Admin, add the HTTPS URL where you want events delivered from Settings → Developer → Webhooks. You can optionally provide a signing secret and up to five custom headers required by your endpoint.
A rep submits a recording, Rilla transcribes and scores it, and the post-processing pipeline marks it done.
We send a single application/json POST to your endpoint with the conversation envelope and your saved custom headers. If you provided a signing secret, the request includes an X-Rilla-Signature header.
Return 2xx after durable acceptance. A failed delivery is eligible for retry only when every matching subscription fails; see Retries and delivery.
Subscribing
Webhook subscriptions are managed from Settings → Developer → Webhooks in the Rilla web app. You'll need an Admin role to access this tab.
From there you can:
- Add a new webhook by entering its HTTPS URL and (optionally) a signing secret and custom headers.
- Edit or remove custom headers on an existing webhook without recreating it.
- Enable or disable a webhook without deleting it.
- Send a test event to confirm your endpoint is reachable and your signature verification works.
- Delete a webhook.
| Field | Required | Description |
|---|---|---|
| URL | Yes | HTTPS URL we should POST events to. Must be reachable from the public internet. |
| Signing secret | No | A shared secret we'll use to HMAC-sign each request. Strongly recommended in production. |
| Custom headers | No | Up to five name/value pairs sent with both live events and test deliveries. See Custom headers for limits. |
Subscriptions registered from the dashboard apply to the entire organization. If you need to scope a subscription to a specific Rilla team, reach out to your account manager.
Use HTTPS
We do not deliver to plain HTTP endpoints. Use a TLS-terminated URL.
Adding a webhook
In the Rilla web app, go to Settings → Developer → Webhooks.
The Webhooks tab lists your organization's existing subscriptions. Click Add webhook to open the inline form.
Enter the HTTPS URL we should deliver events to and optionally set a signing secret. Under Custom headers (optional), select Add header and enter each header's name and value. Click Add webhook to save the subscription. Delivery starts with the next conversation that finishes processing.
Custom headers
Use custom headers when your receiver requires an authentication credential or routing identifier. For example:
| Header name | Example value |
|---|---|
client_id | your-client-id |
Authorization | Bearer YOUR_RECEIVER_TOKEN |
These are placeholders: use the names and values required by your receiving service, not your Rilla API key. Header values are stored encrypted and masked in the editor. Access to saved headers is restricted to admins in your organization.
To update an existing subscription, select Edit headers beside its endpoint. Add or change rows, or select Remove to delete a header, then Save headers. Removing every row and saving clears all custom headers. Cancel leaves the saved configuration unchanged. Save changes before sending a test; tests and live deliveries use the saved headers.
The same validation applies when creating or editing headers:
- A maximum of five custom headers per subscription.
- Header names must be unique, ignoring case:
client_idandClient_IDcount as the same name. - Names and values must be valid HTTP headers. Names cannot contain spaces, and values cannot contain carriage returns or line breaks.
- The complete header map must fit within 4096 UTF-8 bytes when serialized as JSON, including names, values, quotes, and separators. This is a total limit, not a per-header limit.
- Reserved names cannot be set, regardless of capitalization:
Content-Type,X-Rilla-Signature,Host,Content-Length,Transfer-Encoding,Connection,Keep-Alive,TE,Trailer,Upgrade,Proxy-Authorization, andProxy-Authenticate.
Custom headers supplement the standard headers; they do not replace Rilla's JSON content type or optional signature. Subscriptions without custom headers continue to work unchanged.
Use the final destination URL
When at least one custom header is configured, Rilla does not follow HTTP redirects for either live events or tests. A 3xx response is a failed delivery. Register the final HTTPS endpoint directly so credentials are not forwarded to another location.
Testing your endpoint
Open the subscription's three-dot Webhook actions menu and select Send test to send a sample conversation_processed-shaped envelope to your URL with event_type: "test". The sample includes your saved custom headers and, if configured, is signed with your real signing secret. Use it to check that your endpoint accepts your headers and signature verification works.
The result reports the HTTP status code your endpoint returned. A 2xx means we received a successful response. Anything else surfaces the status code or error so you can debug.
Payload
Every event is a single JSON object with a stable envelope and an event_type-specific data block.
Headers
| Header | Description |
|---|---|
Content-Type | Always application/json. |
X-Rilla-Signature | sha256=<hex> HMAC of the raw body, using your signing secret. Only present if you registered a signing secret. See Verifying signatures. |
| Your custom headers | Saved name/value pairs for this subscription, included in live and test requests. See Custom headers. |
Envelope
| Field | Type | Description |
|---|---|---|
webhook_event_id | string (uuid) | ID generated for this subscription delivery attempt. It can change on retry. Use stable business identifiers such as conversation_id for idempotent writes, not this ID. |
event_type | string | conversation_processed for real events, or test when triggered using Send test in the dashboard. Test events use synthetic data and can contain null values such as user_id; do not use a test payload as evidence of real account data. |
sent_at | string (ISO 8601) | Server time when this attempt was constructed. |
data | object | Event-specific payload. See below. |
conversation_processed data
| Field | Type | Description |
|---|---|---|
conversation_id | string (uuid) | Rilla conversation ID. Stable across redeliveries. |
organization_id | string (uuid) | Your Rilla organization ID. |
conversation_url | string | Direct link to the conversation in the Rilla web app. |
transcript_url | string | null | Presigned link to the conversation's transcript as JSON (an array of speaker-tagged words with timings). The link expires about 6 hours after the event is sent, so fetch or store it promptly. null if a transcript isn't available. |
user_id | string (uuid) | Rilla user (the rep) the conversation belongs to. |
user_email | string | Email of the rep. |
recorded_at | string (ISO 8601) | When the recording was made. |
recording_duration_seconds | number | Length of the recording, in seconds. |
summary | string | null | AI-generated conversation summary. null if not generated. |
custom_insights | string | null | AI-generated custom insights configured for your org. null if none. |
surveys | array | Per-survey results for any surveys configured on the org. See Survey object. |
appointment_id | string | null | The associated external CRM appointment ID, if any. |
customer | object | null | Linked customer contact: nullable name, email, and phone. Null when no contact fields exist. Unlike Data Export’s customer object, this webhook object does not include an address. |
Survey object
| Field | Type | Description |
|---|---|---|
surveyId | string (uuid) | Survey definition ID. |
conversationId | string (uuid) | Same conversation ID as the envelope. |
surveyName | string | Human-readable name of the survey. |
createdAt | string (ISO 8601) | When this survey result was generated. |
data | object | Free-form survey responses, keyed by question. Shape depends on your survey definition. |
Example body
{
"webhook_event_id": "8d3e1c4a-7b2f-4a13-b3a5-2c5d49ce4f10",
"event_type": "conversation_processed",
"sent_at": "2026-05-08T19:42:11.418Z",
"data": {
"conversation_id": "f1a3e9d8-2b46-4c1a-9c7e-7e4a18d2c93f",
"organization_id": "1907eddc-64da-4c60-9cc2-f30841635050",
"conversation_url": "https://app.rillavoice.com/conversations/single?id=f1a3e9d8-2b46-4c1a-9c7e-7e4a18d2c93f",
"transcript_url": "https://rillavoice-processed-audio.s3.us-east-1.amazonaws.com/1907eddc-64da-4c60-9cc2-f30841635050/c986c3da-30ba-4d07-8fb8-b8ed38ca7241/1780249759000_transcript.json?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=21600&X-Amz-Signature=EXAMPLE",
"user_id": "a52b8c7d-3e1f-4a90-9b22-6d18e7f4c521",
"user_email": "rep@yourcompany.com",
"recorded_at": "2026-05-08T18:35:00.000Z",
"recording_duration_seconds": 2143,
"summary": "Customer expressed interest in the premium package and asked about financing. Rep walked through the 36-month plan and scheduled a follow-up for next week.",
"custom_insights": "Strong buying signal at 12:30. Pricing objection handled cleanly. Follow-up date confirmed.",
"surveys": [
{
"surveyId": "5e0c3bac-6737-4fdc-a7bc-acbb7f4ccd6f",
"conversationId": "f1a3e9d8-2b46-4c1a-9c7e-7e4a18d2c93f",
"surveyName": "Discovery Call Quality",
"createdAt": "2026-05-08T19:41:55.802Z",
"data": {
"rapport_built": "yes",
"discovery_questions_asked": 7,
"next_step_confirmed": true
}
}
],
"appointment_id": "appt-1234",
"customer": { "name": "Jordan Lee", "email": "jordan@example.com", "phone": null }
}
}Verifying signatures
If you registered a signing secret, every request includes an X-Rilla-Signature header:
X-Rilla-Signature: sha256=4f1ce1e3d9a7b2c45e7c1a89f3b4d2e6...The hex value after sha256= is the HMAC-SHA256 of the raw request body using your shared secret. To verify:
- Read the raw body bytes (do not re-serialize the parsed JSON — whitespace and key order matter).
- Compute
HMAC-SHA256(secret, raw_body)and hex-encode it. - Compare it to the value in the header using a constant-time comparison.
If the values don't match, reject the request.
Always verify the signature in production. Without it, anyone who learns your endpoint URL could forge events. Rilla never delivers to non-HTTPS endpoints, but TLS alone doesn't authenticate the sender.
Python verification
import hmac
import hashlib
def verify(raw_body: bytes, header: str, secret: str) -> bool:
if not header.startswith("sha256="):
return False
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header.removeprefix("sha256="))Node.js verification
import { createHmac, timingSafeEqual } from "crypto";
function verify(rawBody, header, secret) {
if (!header?.startsWith("sha256=")) return false;
const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
const provided = header.slice("sha256=".length);
if (expected.length !== provided.length) return false;
return timingSafeEqual(Buffer.from(expected), Buffer.from(provided));
}Retries and delivery
Return 2xx within 10 seconds after verifying and durably accepting the event. Process lengthy work asynchronously. Delivery is best-effort; exact retry counts and timing depend on queue configuration and are not a published delivery guarantee.
| Behavior | What to expect |
|---|---|
| Success | A 2xx response within the request timeout. |
| Failure | Non-2xx, network error, or timeout. |
| Multiple subscriptions | The conversation is retried only if every matching subscription fails. A successful subscriber prevents independent retries for failed subscribers. |
| Final failure | Repeated failures can reach Rilla’s dead-letter queue. Contact your account manager for recovery. |
| Ordering | Not guaranteed. |
Idempotency
Do not depend on webhook_event_id remaining stable between attempts. Make downstream writes idempotent using stable business identifiers such as data.conversation_id (and your destination record identity). A retry can occur after you processed a request but the response was lost. If you accept later reprocessing updates, update the existing record rather than permanently dropping every future event for that conversation.
What to do when your endpoint is down
Reconcile missing records with the Data Export API. For prolonged outages or replay requests, contact your account manager. Do not assume retries alone recover every missed subscription delivery.
Code examples
Python (FastAPI)
import hmac
import hashlib
from fastapi import FastAPI, Header, HTTPException, Request
app = FastAPI()
SECRET = "your-shared-secret"
@app.post("/rilla/webhook")
async def rilla_webhook(
request: Request,
x_rilla_signature: str = Header(None),
):
raw_body = await request.body()
if x_rilla_signature is None or not x_rilla_signature.startswith("sha256="):
raise HTTPException(401, "missing signature")
expected = hmac.new(SECRET.encode(), raw_body, hashlib.sha256).hexdigest()
provided = x_rilla_signature.removeprefix("sha256=")
if not hmac.compare_digest(expected, provided):
raise HTTPException(401, "bad signature")
event = await request.json()
if event["event_type"] == "conversation_processed":
conversation_id = event["data"]["conversation_id"]
# ... upsert downstream data using conversation_id ...
return {"ok": True}Node.js (Express)
import express from "express";
import { createHmac, timingSafeEqual } from "crypto";
const app = express();
const SECRET = "your-shared-secret";
// Capture the raw body — express.json() drops it.
app.use(
express.json({
verify: (req, _res, buf) => {
req.rawBody = buf;
},
}),
);
app.post("/rilla/webhook", (req, res) => {
const header = req.header("X-Rilla-Signature") ?? "";
if (!header.startsWith("sha256=")) return res.status(401).end();
const expected = createHmac("sha256", SECRET)
.update(req.rawBody)
.digest("hex");
const provided = header.slice("sha256=".length);
if (
expected.length !== provided.length ||
!timingSafeEqual(Buffer.from(expected), Buffer.from(provided))
) {
return res.status(401).end();
}
const event = req.body;
if (event.event_type === "conversation_processed") {
// ... upsert downstream data using event.data.conversation_id ...
}
res.status(200).end();
});Smoke test with cURL
Save the complete example body above as event.json. Set WEBHOOK_SECRET from your receiver’s secret manager. Sign the exact file bytes—an example or truncated signature will not verify.
signature=$(python3 -c 'import os,hmac,hashlib,pathlib; print(hmac.new(os.environ["WEBHOOK_SECRET"].encode(), pathlib.Path("event.json").read_bytes(), hashlib.sha256).hexdigest())')
curl --request POST 'https://your-app.example.com/rilla/webhook' \
--header 'Content-Type: application/json' \
--header "X-Rilla-Signature: sha256=$signature" \
--data-binary @event.jsonSupport
Questions, signing secret rotations, replay requests, or signup: contact your Rilla account manager or support@rilla.com.