RinghouseAPI reference
Get started

API Reference

Webhooks

We POST each event to your HTTPS endpoint the moment it happens, signed, and keep retrying for about a day if you are down.

How delivery works

Add an endpoint in Settings → Webhooks, or with Create a webhook, and pick the events it should hear. When one happens we send a POST with a JSON body to your URL.

Answer with any 2xx within 10 seconds and the delivery is done. Anything else — another status, a timeout, a refused connection — counts as a failure and is retried.

Tip

Answer first, work later. Put the event on a queue and return 200 straight away; a slow handler reads to us as a failed one.

Events

EventWhen it firesdata
message.receivedA text arrives on one of your numbers.A message
message.deliveredThe carrier confirms a text you sent arrived.A message
message.failedThe carrier gives up on a text you sent.A message
call.ringingA call starts ringing, in or out.A call
call.answeredSomeone picks up.A call
call.completedA call ends, answered or not.A call
call.missedAn inbound call ends without being answered. Sent alongside call.completed.A call
call.recording.completedA call's recording is ready to fetch.A call with recording
call.voicemail.completedA caller left a voicemail; the recording is the voicemail.A call with recording
call.transcript.completedA call's transcript is ready.A call with transcript
call.summary.completedA call's AI summary is ready.A call with summary
contact.createdA contact is added to the workspace.A contact
contact.updatedA workspace contact changes.A contact
contact.deletedA workspace contact is removed.The contact as it was

Calls between teammates are internal and never fire. Contacts fire only for contacts shared with the workspace — the ones List contacts returns.

The payload

Every event arrives in the same envelope. data is the same object the matching GET endpoint returns, so you parse one shape whichever way you read it.

{
  "id": "0f6e2d1c-9a8b-4e7f-b3c2-1d0e9f8a7b6c",
  "type": "message.received",
  "apiVersion": "2026-09-25",
  "createdAt": "2026-09-25T14:02:11Z",
  "data": {
    "id": "8c1d4e7a-5b2f-4a90-9e1c-3f7d2b6a1c08",
    "createdAt": "2026-09-25T14:02:11Z",
    "direction": "inbound",
    "from": "+447700900123",
    "media": [
      {
        "contentType": "image/jpeg",
        "url": "https://files.ringhouse.ai/media/8c1d4e7a/photo.jpg"
      }
    ],
    "numberId": "e41b09c2-77d3-4c5e-b0a8-9d2f61c4e3a7",
    "status": "received",
    "text": "Is my table ready for eight?",
    "to": "+441134960000"
  }
}
  • id — the event's id. An event sent to two of your endpoints has the same id at both.
  • type — one of the events above.
  • apiVersion — the payload's version. Fields may be added within a version; none are removed or renamed.
  • createdAt — when the event happened, not when it was sent.

A call event carries the call, plus whatever that event is about:

{
  "id": "0f6e2d1c-9a8b-4e7f-b3c2-1d0e9f8a7b6c",
  "type": "call.recording.completed",
  "apiVersion": "2026-09-25",
  "createdAt": "2026-09-25T14:02:11Z",
  "data": {
    "id": "8c1d4e7a-5b2f-4a90-9e1c-3f7d2b6a1c08",
    "createdAt": "2026-09-25T14:02:11Z",
    "direction": "inbound",
    "duration": 184,
    "endedAt": "2026-09-25T14:05:15Z",
    "from": "+447700900123",
    "hasRecording": true,
    "hasSummary": false,
    "hasTranscript": false,
    "isVoicemail": false,
    "missed": false,
    "numberId": "e41b09c2-77d3-4c5e-b0a8-9d2f61c4e3a7",
    "startedAt": "2026-09-25T14:02:11Z",
    "status": "completed",
    "to": "+441134960000",
    "recording": {
      "duration": 184,
      "url": "https://files.ringhouse.ai/recordings/8c1d4e7a/recording.wav"
    }
  }
}

Verifying signatures

Each delivery is signed with your endpoint's secret, following the Standard Webhooks spec, so any of its libraries will verify it as-is. Three headers come with every request:

HeaderWhat it holds
webhook-idThe delivery's id. The same on every retry of it.
webhook-timestampWhen this attempt was sent, in Unix seconds.
webhook-signaturev1, followed by the base64 HMAC-SHA256 signature.

To check one, take the secret after its whsec_ prefix and base64-decode it. Sign {webhook-id}.{webhook-timestamp}.{body} with HMAC-SHA256, base64 the result, and compare it with the signature in constant time. Reject timestamps more than five minutes from your clock, so a captured request cannot be replayed later.

import crypto from "node:crypto";

// `body` is the raw request body as a string, before any JSON parsing.
export function verify(secret, headers, body) {
  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = crypto
    .createHmac("sha256", key)
    .update(`${id}.${timestamp}.${body}`)
    .digest();

  return headers["webhook-signature"].split(" ").some((entry) => {
    const given = Buffer.from(entry.split(",")[1] ?? "", "base64");
    return given.length === expected.length && crypto.timingSafeEqual(given, expected);
  });
}
Warning

Verify the raw body exactly as it arrived. Parsing the JSON and serialising it again changes the bytes, and the signature will never match.

Find the secret on the webhook's page in settings. Rotating it takes effect on the next delivery.

Retries

A failed delivery is tried again on this schedule, about 27 hours in all:

AttemptWaits after the one before
25 seconds
35 minutes
430 minutes
52 hours
65 hours
710 hours
810 hours

If the eighth attempt fails, the webhook is stopped: nothing more is sent to it, and the workspace's admins get an email saying so. Turn it back on in settings, or with Update a webhook and enabled: true. Deliveries missed while it was stopped are not replayed, but you can retry any one from its log.

Duplicates and order

Delivery is at least once. A retry after a timeout can reach you twice if your server did the work but answered too late. Keep the webhook-id of what you have handled and skip one you have seen.

Events are sent in the order they happened, but a retry can land after a newer event. Order by createdAt, or fetch the resource when you need its latest state.

Filtering by number

Give an endpoint numberIds and it only hears events on those numbers; leave it out to hear every number, including ones added later. Contact events belong to the workspace rather than a number, so every endpoint subscribed to them hears them.

Testing an endpoint

Send test on the webhook's page, or Test a webhook, sends a signed webhook.test event within a few seconds. It is never sent otherwise, so it is safe to ignore in production. Every delivery, with the status and time of its last attempt, is in the webhook's log.

Was this page helpful?