How it works
When something happens in Miraven, like a call finishing, we send an event to your endpoint: a POST with a JSON body, signed so you can check it came from us.
A caller hangs up
The funeral home’s agent finishes the call. Miraven processes it and saves the recording.
Miraven sends an event
Your endpoint receives a signed
call.analyzedevent, usually within a few minutes of the call ending.You verify it and respond
Check the signature, record the event, and answer with a
2xxwithin 15 seconds. Do slow work, like downloading the recording, afterwards.
POST /webhooks/miraven HTTP/1.1
Host: api.example.com
Content-Type: application/json
User-Agent: Miraven-Webhooks/1.0
webhook-id: evt_2b7c41e09f6a4b1d8f3e5a7c9d0b1e2f
webhook-timestamp: 1762197124
webhook-signature: v1,33DcwBe0+lAhKqXAgY05/QROBF4z3Xnd7cyJbTZaURY=
{
"id": "evt_2b7c41e09f6a4b1d8f3e5a7c9d0b1e2f",
"type": "call.analyzed",
"created_at": "2026-11-03T19:12:04.000Z",
"data": {
"call_id": "6f1d2c3b-8a9e-4f70-b1c2-d3e4f5a6b7c8",
"agent_id": "39d63614-2b1a-4c5d-9e8f-7a6b5c4d3e2f",
"caller_phone": "+15551234567",
"called_phone": "+15557654321",
"forwarded_from": "+15553334444",
"funeral_home_phone": "+15553334444",
"started_at": "2026-11-03T19:05:22.000Z",
"ended_at": "2026-11-03T19:11:40.000Z",
"duration_seconds": 378,
"recording": {
"url": "https://app.miraven.com/api/recordings/6f1d2c3b-8a9e-4f70-b1c2-d3e4f5a6b7c8?exp=1777835524&sig=EXAMPLE",
"content_type": "audio/wav",
"expires_at": "2027-05-04T19:12:04.000Z"
}
}
}Set up your endpoint
Endpoints are set up by the Miraven team for now. Email support@miraven.com with your endpoint URL and the funeral homes you’re integrating with. We’ll send you:
- A signing secret starting with
whsec_, for verifying requests. Store it like a password. - The agent IDs for those funeral homes. Every event carries one, so one endpoint can serve several funeral homes.
Endpoint URL requirements
- HTTPS with a valid certificate. Plain
http://URLs aren’t accepted. - Publicly reachable. Use a tunnel such as ngrok to test a local server.
- The final URL. Redirects aren’t followed and count as a failed delivery.
- No credentials in the URL. If your system needs an API key, we can send it in a header on every request, stored encrypted on our side.
Create a handler
Your handler verifies the signature, skips events it has already handled, queues the work, and responds. These examples use the Standard Webhooks library, which implements Miraven’s signature scheme.
import express from "express";
import { Webhook } from "standardwebhooks";
const app = express();
const webhook = new Webhook(process.env.MIRAVEN_WEBHOOK_SECRET); // "whsec_…"
// express.raw keeps the body exactly as it arrived, which verification needs.
app.post("/webhooks/miraven", express.raw({ type: "application/json" }), async (req, res) => {
let event;
try {
event = webhook.verify(req.body, req.headers);
} catch {
return res.sendStatus(400);
}
// Retries and resends deliver the same event again, with the same id.
if (await db.processedEvents.has(event.id)) return res.sendStatus(200);
switch (event.type) {
case "call.analyzed":
await queue.add("miraven-call", event.data); // download the recording later, not here
break;
default:
// Ignore event types you don't handle; we add new ones over time.
break;
}
await db.processedEvents.add(event.id);
res.sendStatus(202);
});
app.listen(3000);Test your handler
Send yourself a test event
This script builds a call.analyzed event, signs it with your secret the way Miraven does, and sends it to your handler. Use your real secret, or generate one for local testing.
# Any whsec_ secret works for local testing:
node -e "console.log('whsec_' + require('crypto').randomBytes(32).toString('base64'))"// send-test-event.mjs
// MIRAVEN_WEBHOOK_SECRET=whsec_… node send-test-event.mjs http://localhost:3000/webhooks/miraven
import crypto from "node:crypto";
const url = process.argv[2];
const secret = process.env.MIRAVEN_WEBHOOK_SECRET;
const id = `evt_test_${crypto.randomUUID().replaceAll("-", "")}`;
const now = new Date();
const body = JSON.stringify({
id,
type: "call.analyzed",
created_at: now.toISOString(),
data: {
call_id: crypto.randomUUID(),
agent_id: "00000000-0000-0000-0000-000000000000",
caller_phone: "+15551234567",
called_phone: "+15557654321",
forwarded_from: "+15553334444",
funeral_home_phone: "+15553334444",
started_at: new Date(now - 378_000).toISOString(),
ended_at: now.toISOString(),
duration_seconds: 378,
recording: {
url: "https://app.miraven.com/api/recordings/test", // test events have no real recording
content_type: "audio/wav",
expires_at: new Date(+now + 182 * 86_400_000).toISOString(),
},
},
});
// Sign it the way Miraven does: HMAC-SHA256 over "id.timestamp.body".
const timestamp = Math.floor(now / 1000);
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const signature = crypto.createHmac("sha256", key).update(`${id}.${timestamp}.${body}`).digest("base64");
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"webhook-id": id,
"webhook-timestamp": String(timestamp),
"webhook-signature": `v1,${signature}`,
},
body,
});
console.log(response.status, await response.text());Expect a 2xx. Send the same event twice to check your handler skips the repeat.
Receive real calls
Before we turn an endpoint on for every call, we send it one or two real calls. Tell us when they’ve arrived and the recordings downloaded, and we’ll switch it on.
Event delivery behaviors
Automatic retries
If your endpoint doesn’t answer with a 2xx within 15 seconds, Miraven retries with exponential back-off for about 3 days. A 4xx other than 408 or 429 stops retries straight away, since it usually means something needs fixing first.
| Attempt | Wait before it | Time since the first attempt |
|---|---|---|
| 1 | None | 0 |
| 2 | 1 minute | 1 minute |
| 3 | 5 minutes | 6 minutes |
| 4 | 30 minutes | 36 minutes |
| 5 | 2 hours | 2.5 hours |
| 6 | 6 hours | 8.5 hours |
| 7 | 12 hours | 20.5 hours |
| 8 | 1 day | 1.9 days |
| 9 | 1 day | 2.9 days |
The Miraven team is alerted when an endpoint has been failing for more than 30 minutes, and again when an event runs out of retries.
Manual retries
Ask support@miraven.com to resend any event, for example after an outage or a bug in your handler. A resent event keeps its id.
Event ordering
Events aren’t guaranteed to arrive in order: a retried event can arrive after a newer one. Order calls by data.started_at, not by when the event arrived.
Debug failed deliveries
| What we see | What it means | Fix |
|---|---|---|
| Could not connect | DNS, TLS or the connection failed. | Check the URL is public, the certificate is valid, and the server is up. |
| No response in 15s | Your handler took too long. | Respond first and do the work in a queue. |
3xx | We don’t follow redirects. | Give us the final URL. |
401 / 403 | Your endpoint rejected the request. | Check your signing secret and any custom header. |
Other 4xx | Your endpoint rejected the request, and we stopped. | Fix the cause, then ask us to resend. |
5xx | Your server errored. | Check your logs. We retry automatically. |
Best practices
Handle duplicate events
Your endpoint can receive the same event more than once, after a retry or a resend. Record the id of each event you process and skip ones you’ve seen. Answer repeats with a 2xx so they aren’t retried.
Only handle the event types you need
We’ll add event types over time. Check type and ignore any you don’t handle, still answering with a 2xx.
Handle events asynchronously
Put each event on a queue and process it separately. Recordings can be large, so download them from the queue, not inside the request.
Quickly return a 2xx response
Respond within 15 seconds, or the delivery counts as failed and is retried. Respond before any slow work.
Verify events are sent from Miraven
Verify the signature on every request before acting on it.
Prevent replay attacks
Reject requests whose webhook-timestamp is more than 5 minutes from your clock. The Standard Webhooks libraries do this for you. Each retry is signed with a new timestamp, so retries still pass.
Exempt your webhook route from CSRF protection
Frameworks like Django and Rails reject POSTs without a CSRF token, which Miraven can’t send. Exempt the webhook route only: the signature protects it instead.
Store the recordings you need
Recording links work for about 6 months, or until the funeral home deletes the call. Download and store recordings you need longer, and don’t log the links: anyone with one can download the recording.