Free live virtual workshop: Set Up AI and Automate Workflows in Your Funeral Home. Fri, Nov 13, 10 AM PT (1 PM ET) Register

Receive Miraven events in your webhook endpoint

Get a signed HTTPS request after each call a funeral home’s agent answers, with the call’s details and a link to its recording.

Updated

View as Markdown

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.

  1. A caller hangs up

    The funeral home’s agent finishes the call. Miraven processes it and saves the recording.

  2. Miraven sends an event

    Your endpoint receives a signed call.analyzed event, usually within a few minutes of the call ending.

  3. You verify it and respond

    Check the signature, record the event, and answer with a 2xx within 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.

AttemptWait before itTime since the first attempt
1None0
21 minute1 minute
35 minutes6 minutes
430 minutes36 minutes
52 hours2.5 hours
66 hours8.5 hours
712 hours20.5 hours
81 day1.9 days
91 day2.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 seeWhat it meansFix
Could not connectDNS, TLS or the connection failed.Check the URL is public, the certificate is valid, and the server is up.
No response in 15sYour handler took too long.Respond first and do the work in a queue.
3xxWe don’t follow redirects.Give us the final URL.
401 / 403Your endpoint rejected the request.Check your signing secret and any custom header.
Other 4xxYour endpoint rejected the request, and we stopped.Fix the cause, then ask us to resend.
5xxYour 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.

See also

Questions? Email support@miraven.com.