Gareth Wilson Gareth Wilson

Guide to CallRail Webhooks: Features and Best Practices

Published


CallRail webhooks push call tracking data into your own systems as calls and leads come in: a call starts ringing, it finishes and its recording and transcript are attached, someone tags it afterwards, a text message arrives, a form is submitted on your site. If you use CallRail to attribute calls to marketing campaigns, webhooks are how that data reaches your CRM, screen-pop tool or reporting pipeline without polling the API.

This guide covers how CallRail webhooks are configured, how to verify the Signature header, how to tell one event from another when the payload doesn't say, and how to build a receiver for a sender that never retries.

What are CallRail webhooks?

CallRail webhooks belong to a company. Each company has one Webhooks integration, and its configuration holds a separate list of URLs for each event: pre-call, call routing complete, post-call, call modified, the two outbound call events, text message received and sent, and form submission. When an event fires, CallRail sends a JSON POST to every URL in that event's list.

The body is the resource itself. A call event sends a call object in the same format as the call API, a text event sends the text message, and a form event sends the form submission. No field or header names the event. The only thing that tells a pre-call webhook from a post-call webhook is the URL it was sent to.

Every request carries a Signature header: a base64 HMAC-SHA1 of the request body, keyed with a secret token CallRail generates for the company.

CallRail webhook features

FeatureDetails
ConfigurationThe Webhooks integration in the CallRail app, or POST /v3/a/{account_id}/integrations.json
Subscription modelOne URL list per event, in the integration's config object
Event identificationNone on the wire. Each event's URL tells you which event it is
Signature headerSignature
SchemeHMAC-SHA1, base64 digest
Signed contentThe request body
SecretOne token per company, on the Webhooks configuration page (signing_key in the API)
Replay windowNone defined. CallRail suggests comparing the body's timestamp with the current time
Success responseAny 2xx
RetriesNone. CallRail does not resend webhooks
Failure handlingRepeated failures can disable the integration automatically

CallRail publishes no source IP list for webhooks.

Common events

EventConfig keyBodyFires when
Pre-Callpre_call_webhookCall objectAn inbound call is received, before it is connected
Call Routing Completeanswered_call_webhookCall objectAn inbound call has been routed to its destination
Post-Callpost_call_webhookCall objectAn inbound call has ended and its recording, transcription and summary are attached
Call Modifiedupdated_call_webhookCall object plus changesA call changes after it has ended, for example a tag, note or spam flag
Outbound Post-Callpost_outbound_call_webhookCall object, call_type is outboundAn outbound call has ended
Outbound Call Modifiedupdated_outbound_call_webhookCall object plus changesAn outbound call changes after it has ended
Text Message Receivedsms_received_webhookText messageA tracking number receives a text
Text Message Sentsms_sent_webhookText message plus agentA tracking number sends a text
Form Submissionform_captured_webhookForm submissionA tracked form is submitted (Form Tracking must be active)

CallRail also documents a Convert Assist Complete webhook, sent after an interaction finishes processing Convert Assist AI answers, with person_resource_id, answers and interaction_count. The configuration table has no key for it, so CallRail does not say how to subscribe to it on its own.

The call events form a sequence, and their timing matters. Pre-call and call routing complete fire in real time with partial data. Post-call is not real time: it waits for the recording, transcription and call summary, up to 20 minutes after hangup, and then fires even if some of that data is still missing. CallRail recommends subscribing to Call Modified too, so that a late recording or transcript still reaches you. Call Modified carries the updated fields in place plus a changes array naming them, such as ["note"] or ["tags"].

Here is CallRail's Text Message Received example:

{
  "id": 879204976,
  "resource_id": "SCI0c14896tw1b64fb970ab62aaf4fbefd9",
  "source_number": "770-555-5555",
  "destination_number": "770-123-4567",
  "content": "CallRail is the best! ",
  "message_type": "sms",
  "media_urls": [],
  "timestamp": "2017-10-23T15:57:18.826-04:00",
  "lead_status": null,
  "conversation_id": "crpLH",
  "company_resource_id": "COM834847e9f4bb4f9h8ll29eb173fe658e",
  "person_resource_id": "PERb122rc301ec45f5yy021d4jj180c1a79"
}

Call bodies are larger. They follow the call API's fields and also include legacy fields such as callernum, trackingnum and datetime alongside the current ones. CallRail asks new integrations to rely on the documented API fields and to parse the JSON body, ignoring the few legacy attributes that also arrive in the query string.

Setting up CallRail webhooks

Because nothing in the payload names the event, give every event its own URL. Either use separate paths, or add a query parameter you control, such as ?event=post_call, and read it in your handler.

  1. In CallRail, open the Webhooks integration for the company and enter a URL for each event you need.
  2. Copy the secret token shown on the Webhooks configuration page and store it server-side, for example as CALLRAIL_SIGNING_KEY.
  3. Make a test call or submit a test form and check that your endpoint returns a 2xx.

To create the integration through the API:

curl -X POST "https://api.callrail.com/v3/a/{account_id}/integrations.json" \
  -H "Authorization: Token token={api_token}" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "Webhooks",
    "company_id": "COM8154748ae6bd4e278a7cddd38a662f4f",
    "config": {
      "post_call_webhook": ["https://example.com/webhooks/callrail?event=post_call"],
      "updated_call_webhook": ["https://example.com/webhooks/callrail?event=updated_call"],
      "form_captured_webhook": ["https://example.com/webhooks/callrail?event=form_captured"]
    }
  }'

The response includes signing_key, the same token as on the configuration page. A company can have only one integration of each type, so to change the URLs, update the existing integration rather than creating another.

Securing CallRail webhooks

Compute an HMAC-SHA1 of the raw request body with the signing key as a plain string, base64-encode it, and compare it to the Signature header in constant time.

const crypto = require('crypto');
const express = require('express');

function verifyCallRailSignature(rawBody, signatureHeader, signingKey) {
  if (!signatureHeader || !signingKey) return false;
  const expected = crypto.createHmac('sha1', signingKey).update(rawBody).digest('base64');
  const received = Buffer.from(signatureHeader, 'utf8');
  const computed = Buffer.from(expected, 'utf8');
  return received.length === computed.length && crypto.timingSafeEqual(received, computed);
}

const app = express();

app.post('/webhooks/callrail', express.raw({ type: '*/*' }), (req, res) => {
  const signingKey = process.env.CALLRAIL_SIGNING_KEY;
  if (!signingKey) return res.sendStatus(500); // fail closed

  if (!verifyCallRailSignature(req.body, req.get('signature'), signingKey)) {
    return res.sendStatus(401);
  }

  const event_name = req.query.event; // the parameter you put on each URL
  const payload = JSON.parse(req.body.toString('utf8'));
  // Persist { event_name, payload }, then process asynchronously.
  res.sendStatus(200);
});

The same check in Python:

import base64
import hashlib
import hmac
import os


def verify_callrail_signature(raw_body: bytes, signature_header, signing_key: str) -> bool:
    if not signature_header or not signing_key:
        return False
    digest = hmac.new(signing_key.encode("utf-8"), raw_body, hashlib.sha1).digest()
    expected = base64.b64encode(digest)
    return hmac.compare_digest(expected, signature_header.encode("utf-8"))


# In FastAPI: raw_body = await request.body()
# verify_callrail_signature(raw_body, request.headers.get("signature"),
#                           os.environ["CALLRAIL_SIGNING_KEY"])

CallRail publishes a test vector. With the key 072e77e426f92738a72fe23c4d1953b4 and the sample call body in the Security section of CallRail's API docs (compact JSON, no trailing newline), your code should produce UZAHbUdfm3GqL7qzilGozGzWV64=. Copy the body exactly; a single added space or newline changes the result.

The signature covers only the body. CallRail notes that each request includes a timestamp you can compare with the current time to reject replays. If you do this, keep it optional and generous: on call events, timestamp is the call's start time, and a post-call webhook can arrive 20 minutes after hangup.

Make CallRail webhooks production-ready. Hookdeck Event Gateway verifies the Signature header, stores every call, text and form event the moment it arrives, and lets you retry or replay deliveries that CallRail itself will never resend.

CallRail webhook limitations and pain points

A failed delivery is gone

The Problem: If your endpoint is down or returns an error, that call or form submission never reaches you.

Why It Happens: CallRail does not resend webhooks. A non-2xx response or a connection failure loses the event, and CallRail warns that "missing calls might be your first indication of a problem". Repeated failures can also disable the integration automatically.

Workarounds:

  • Keep the endpoint simple: verify, store, return 2xx, and process later.
  • Reconcile periodically against the CallRail API for the calls, texts and forms you expected.
  • Monitor the integration's state so you notice if CallRail disables it.

How Hookdeck Can Help: Hookdeck accepts each delivery as soon as it arrives and keeps it, then retries against your service until it succeeds. Your downtime becomes a delay instead of lost leads, and fewer failed responses reach CallRail.

No event type in the payload

The Problem: Your handler receives a call object and can't tell whether it is a pre-call, post-call or call modified webhook.

Why It Happens: CallRail sends the resource without an envelope. The event is decided by which URL list you put the endpoint in, and nothing in the request repeats it.

Workarounds:

  • Register a distinct path or query parameter per event.
  • If you must share one URL, infer from the shape: a changes array means call modified, call_type: "outbound" means an outbound event, message_type means a text, and form_data means a form. This cannot separate pre-call from post-call reliably.

How Hookdeck Can Help: Hookdeck filters can match on the request's path and query as well as its body, so a per-event parameter on each CallRail URL is enough to route each event to the right destination.

Post-call data arrives late or incomplete

The Problem: Recordings, transcripts or summaries are missing from the post-call payload, or arrive long after the call.

Why It Happens: Post-call waits for recording, transcription and summary processing for up to 20 minutes, then fires with whatever is ready.

Workarounds:

  • Subscribe to Call Modified and update the stored call when changes lists recording, transcription_text or call_summary.
  • Use pre-call or call routing complete for anything that must happen during the call.

How Hookdeck Can Help: Hookdeck's filters and routing can send post-call and call modified events to the same handler and hold them for replay if that handler fails.

Best practices

Verify against the raw body

The signature is computed over the exact bytes CallRail sent. Read the raw body, verify, then parse. Test your code against CallRail's published vector before going live.

Give each event its own URL

Use separate paths or a query parameter so that every request identifies its own event. It makes routing, logging and replay much simpler.

Treat the call as a record that changes

Store calls keyed on id and update them in place as later events arrive. A call can produce pre-call, routing complete, post-call and several call modified webhooks.

Deduplicate deliveries

There is no delivery ID. Key text messages on id or resource_id, forms on id, and calls on id plus the event name, so a repeated delivery of the same event updates rather than duplicates. See our guide to webhook idempotency.

Acknowledge fast and process asynchronously

Because CallRail never retries, a slow or failing handler costs data. Return 2xx as soon as the event is stored. See why to process webhooks asynchronously.

Ignore the query string fields

Legacy attributes also arrive as query parameters. CallRail only adds and updates fields in the JSON body, so read the body and ignore the query string, apart from any parameter you added yourself.

Conclusion

CallRail signs each webhook with a base64 HMAC-SHA1 of the body in the Signature header, keyed with the company's signing token. The payload never names the event, so give each event its own URL, and because CallRail never resends a failed webhook, acknowledge quickly and keep the endpoint available.

Hookdeck Event Gateway sits in front of your receiver, verifies every Signature, and keeps each call, text and form event until your service has processed it, adding the retries CallRail doesn't provide.

Get started with Hookdeck for free and handle CallRail webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.