Skip to content
Esc
navigateopen⌘Jpreview
On this page

Receive events with webhooks

Subscribe an HTTPS endpoint to Steer Phones call and voicemail events, verify the signature, and handle retries.

A webhook endpoint is an HTTPS URL you own that Steer Phones posts to when something happens on a call. It is the fastest way to make another system react in real time — pop a customer record when the phone rings, log an activity when the call ends, create a task when a voicemail lands — without polling for changes.

Webhooks work independently of the request-response API and need no API key: each delivery proves it came from Steer Phones by signing the request with a secret shared only with your endpoint.

Events you can subscribe to

An endpoint subscribes to one or more event types, and receives only those.

Event type Fires when Typical use
call.incoming A call arrives, before routing decides where it goes. Screen pop
call.ringing The call starts ringing a destination. Live wallboard
call.answered Someone answers. Start a timer, open a record
call.on_hold The call is placed on hold. Hold-time monitoring
call.ended The call finishes, with its outcome and duration. Activity logging
voicemail.created A voicemail is recorded. Task or ticket creation
call.analysis.completed AI analysis of a call finishes. Scoring and coaching workflows

Call lifecycle events are delivered ahead of other events when both are queued, because a screen pop is worthless if it arrives after the call is over.

What a delivery looks like

Steer Phones sends an HTTP POST with a JSON body in a consistent envelope. The event-specific content is always under data; the envelope around it never changes shape.

{
  "id": "6f1c0f2e-6a1f-4f0f-9a10-2b0f4c9a71d3",
  "type": "call.incoming",
  "timestamp": "2026-08-03T15:04:05.000Z",
  "phoneSystem": "b3f1a5c2-1d44-4c19-9a1e-2f7d8c4b0e11",
  "data": {
    "callSid": "CAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "direction": "inbound",
    "from": "+15555550123",
    "to": "+15555550100",
    "startTime": "2026-08-03T15:04:04.000Z",
    "extension": "0f6a1c2e-71d3-4b19-8a10-2b0f4c9a5f22"
  }
}
  • id identifies this delivery. It stays the same across retries, so use it to make your handler idempotent. The same underlying event sent to two endpoints has a different id for each.
  • type is the event type from the table above.
  • timestamp is when the event occurred, in UTC.
  • phoneSystem is your phone system’s identifier.
  • data carries the event’s fields. Call events include the call identifier, direction, the numbers involved, and — where known — the extension or call group and the duration or outcome. Voicemail events include the voicemail identifier, the caller, and the recording length.

Fields can be added to data over time, so ignore members you do not recognize rather than rejecting the delivery.

Each request carries these headers:

Header Value
Content-Type application/json
X-Steer-Phones-Signature Hex-encoded HMAC-SHA256 signature of the request.
X-Steer-Phones-Timestamp Unix time, in seconds, when this attempt was signed.
User-Agent SteerPhones-Webhook/1.0

Verify every delivery

Your endpoint is a public URL, so anyone can post to it. Verify the signature before you act on a payload, and reject anything that fails.

  1. Read the raw request body as bytes, exactly as received. Do not parse and re-serialize it first — re-serializing changes whitespace and key order, and the signature will not match.
  2. Read X-Steer-Phones-Timestamp and build the string <timestamp>.<raw body>.
  3. Compute HMAC-SHA256 over that string using your endpoint’s signing secret, hex-encoded.
  4. Compare it to X-Steer-Phones-Signature with a constant-time comparison.
  5. Reject deliveries whose timestamp is far outside your tolerance — a few minutes is a reasonable window — so a captured request cannot be replayed later.
const { createHmac, timingSafeEqual } = require('node:crypto');

function isValidSteerWebhook(rawBody, headers, secret) {
  const timestamp = headers['x-steer-phones-timestamp'];
  const signature = headers['x-steer-phones-signature'];
  if (!timestamp || !signature) return false;

  const ageSeconds = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
  if (!Number.isFinite(ageSeconds) || ageSeconds > 300) return false;

  const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
  if (expected.length !== signature.length) return false;

  const expectedBuf = Buffer.from(expected, 'hex');
  const signatureBuf = Buffer.from(signature, 'hex');
  if (expectedBuf.length !== signatureBuf.length) return false;

  return timingSafeEqual(expectedBuf, signatureBuf);
}

Each retry is signed fresh, so the timestamp and signature differ between attempts of the same delivery while the body — and therefore the envelope id — stays the same. Deduplicate on id, not on the signature.

Store the signing secret the way you store any credential: in a secret manager, never in source control or a browser. If it is exposed, ask Steer Phones to regenerate it; the previous secret stops working immediately, so deploy the new value promptly.

Delivery, retries, and auto-disable

  • Respond fast. Return a 2xx status within 15 seconds. Anything slower is recorded as a timeout. Acknowledge first and do your real work asynchronously.
  • Any 2xx counts as success. The response body is ignored; only the status matters.
  • Failures are retried. A delivery is attempted up to five times with exponentially increasing delays, spanning roughly fifteen minutes. After the last attempt it is marked failed and not retried again.
  • Retries can arrive out of order. A retried event may land after a later event. Use the envelope timestamp to order what you store, not arrival order.
  • Redirects are not followed. A 301 or 302 counts as a failure. Register the final URL.
  • Repeated failures disable the endpoint. Consecutive failures across deliveries are counted, and once they reach the endpoint’s threshold — ten by default — the endpoint is automatically disabled and stops receiving events until it is re-enabled. A single success resets the counter, so an endpoint that is merely flaky is not disabled.

An endpoint disabled this way is a silent outage in your integration if nobody is watching for it. Alert on “no Steer Phones events received recently” on your side rather than relying on noticing the gap.

Endpoint requirements

  • HTTPS only. Plain HTTP is rejected.
  • A publicly resolvable host. Localhost and private or reserved addresses are rejected, so an endpoint behind a corporate firewall needs a public tunnel or gateway.
  • Up to 25 endpoints per phone system, each with its own subscription list and its own secret.
  • A stable URL. Changing the URL is an update to the endpoint, not a redirect from the old one.

Separate endpoints are worth using when different systems want different events — a CRM subscribing to call lifecycle events and a ticketing system subscribing only to voicemail, each with its own secret, so revoking one does not disturb the other.

Ask Steer Phones to register an endpoint

Endpoint registration is handled by Steer Phones today; there is no webhook page in the dashboard. Send your Steer Phones contact:

  1. The phone system the endpoint is for.
  2. The HTTPS URL to deliver to.
  3. The event types to subscribe to.
  4. A short description, so the endpoint is recognizable later.
  5. Whether you want expanded entity data (below).

Steer Phones registers the endpoint, returns the signing secret once, and can send a test event to it — a test.ping delivery signed exactly like a real one, so you can confirm your verification code works before any live call depends on it. Steer Phones can also enable, disable, or delete an endpoint, regenerate its secret, and review the delivery history for it — including the response status and timing of each attempt — when you need to know whether a specific event reached you.

Expanded entity data

By default, references inside data are identifiers: extension and callGroup are UUID strings. An endpoint can instead be set to include expanded data, replacing each identifier with a small object — an extension’s number, name, and status; a call group’s name, number, ring pattern, and member extensions; the phone system’s name and time zone.

Turn it on when the receiving system has no other way to resolve those identifiers and would otherwise have to call back for every event. Leave it off when your integration already has that information, since it makes every payload larger.

Troubleshooting

  • Signature never matches: You are almost certainly verifying a re-serialized body. Capture the raw bytes before any JSON middleware parses them.
  • Nothing arrives at all: Confirm the endpoint is enabled and not auto-disabled after a run of failures, that the URL is publicly reachable over HTTPS, and that the endpoint subscribes to the event type you expect.
  • Events arrive but one type is missing: The subscription list does not include it. Subscriptions are per endpoint.
  • Duplicate events: Retries and multiple endpoints both produce more than one request. Deduplicate on the envelope id.
  • Deliveries stop after an incident on your side: The endpoint was likely auto-disabled. Ask Steer Phones to re-enable it once your endpoint is healthy; the failure counter is reset when it is.
  • Events fire during test calls only: Check that the phone system in phoneSystem is the one you intend to integrate with — each system’s events go to its own endpoints.