Webhooks

Webhooks let your own systems hear about activity in CurrentClient the moment it happens — a new message landing in an inbox, a call wrapping up, a voicemail coming in — instead of you having to poll the API and ask.

Setting up an endpoint

  1. Log into CurrentClient and open Settings.
  2. Click Developers in the left sidebar, then Webhooks.
  3. Click Add endpoint.
  4. Give it a Name (just for you, so you can tell it apart from any others) and the Endpoint URL we should POST to.
  5. Pick which events you want to receive — see the full list below.
  6. Save it. Your endpoint gets its own signing secret the moment it's created — copy it somewhere safe (you can also view it again any time with Reveal on the endpoint's card). We use it to sign every delivery so you can confirm it really came from us (more on that below).

This screen only manages WEBHOOK-type endpoints — it's separate from the Zapier integration, which handles its own subscriptions.

Events you can subscribe to

Every delivery is a single POST request with a JSON body, a Content-Type: application/json header, and an X-CC-Event-Type header carrying the same event type as the body — handy if you want to route on the header without parsing JSON first. This is the current catalog, with an example payload for each.

Every event listed below carries a matching top-level "event_type" field in the body (in addition to the X-CC-Event-Type header) — it's shown first in each example because that's genuinely where it sits on the wire.

message.received

Fires when an inbound message lands on one of your routed inboxes.

{
  "event_type": "message.received",
  "id": "MSG-4f9c2a10",
  "phone_number": "+15551234567",
  "sender_name": "Jane Doe",
  "message": "Thanks, see you then!",
  "timestamp": "2026-09-08T14:32:10+00:00",
  "media_urls": []
}

media_urls is an empty list unless the message included attachments, in which case it's a list of URLs.

message.sent

Fires when an outbound message you sent is confirmed sent. Subscribe to this alongside message.received to see the full conversation, not just the inbound half.

{
  "event_type": "message.sent",
  "id": "MSG-9c31af02",
  "phone_number": "+15551234567",
  "contact_name": "Jane Doe",
  "message": "See you at 3pm",
  "timestamp": "2026-09-08T14:32:10+00:00",
  "media_urls": []
}

media_urls is an empty list unless the message included attachments, in which case it's a list of URLs.

call.missed

Fires when an inbound call goes unanswered.

{
  "event_type": "call.missed",
  "id": "CALL-7a12e903",
  "phone_number": "+15551234567",
  "caller_name": "Jane Doe",
  "timestamp": "2026-09-08T14:32:10+00:00",
  "duration": "0"
}

call.completed

Fires when any call — inbound or outbound, answered or not — finishes.

{
  "event_type": "call.completed",
  "id": "CALL-7a12e903",
  "phone_number": "+15551234567",
  "caller_name": "Jane Doe",
  "direction": "inbound",
  "status": "completed",
  "timestamp": "2026-09-08T14:32:10+00:00",
  "duration": "125",
  "note": "",
  "summary": ""
}

note and summary are empty strings when nothing's been added yet.

voicemail.received

Fires when a caller leaves a voicemail.

{
  "event_type": "voicemail.received",
  "id": "CALL-7a12e903",
  "phone_number": "+15551234567",
  "caller_name": "Jane Doe",
  "duration": "42",
  "audio_url": "https://media.currentclient.com/recordings/abc123.mp3",
  "transcription": "Hey, it's Jane, calling to confirm...",
  "timestamp": "2026-09-08T14:32:10+00:00"
}

callrecording.created

Fires when a call recording finishes processing and is ready to fetch.

{
  "event_type": "callrecording.created",
  "id": "RE1234567890abcdef1234567890abcdef",
  "call_id": "CALL-7a12e903",
  "phone_number": "+15551234567",
  "caller_name": "Jane Doe",
  "recording_url": "https://media.currentclient.com/recordings/abc123.mp3",
  "duration": "125",
  "timestamp": "2026-09-08T14:32:10+00:00",
  "transcription": "Full call transcript goes here..."
}

calltranscript.completed

Fires when a call's full transcript finishes processing (distinct from callrecording.created, which fires when the audio itself is ready).

{
  "event_type": "calltranscript.completed",
  "id": "CALL-7a12e903",
  "phone_number": "+15551234567",
  "caller_name": "Jane Doe",
  "direction": "inbound",
  "duration": "125",
  "transcript": "Full call transcript goes here...",
  "timestamp": "2026-09-08T14:32:10+00:00"
}

callsummary.completed

Fires when the AI-generated summary of a call finishes processing.

{
  "event_type": "callsummary.completed",
  "id": "CALL-7a12e903",
  "phone_number": "+15551234567",
  "caller_name": "Jane Doe",
  "direction": "inbound",
  "duration": "125",
  "summary": "Client confirmed the Tuesday meeting and asked about rescheduling next month's review.",
  "timestamp": "2026-09-08T14:32:10+00:00"
}

survey.submitted

Fires when a contact submits one of your survey/form campaigns.

{
  "event_type": "survey.submitted",
  "id": "PLOP-9f21ac04",
  "contact_id": "CONTACT-a83f1290",
  "contact_name": "Jane Doe",
  "campaign_id": "CAMPAIGN-1120",
  "campaign_title": "New Client Intake",
  "campaign_url": "https://app.currentclient.com/app/surveys/CAMPAIGN-1120",
  "responses": [
    { "id": "COMP-1", "label": "What's the best way to reach you?", "response": "Text message" },
    { "id": "COMP-2", "label": "Referred by", "response": "" }
  ],
  "timestamp": "2026-09-05 20:14:03"
}

responses is a list, one entry per form field, in the order the campaign defines them; response is "" for a field the contact left blank. Note: unlike every other event on this page, timestamp here is passed through as recorded rather than normalized to ISO 8601 — don't assume the format matches the other events' timestamps when parsing this one.

message.undelivered

Fires when an outbound message you sent fails to deliver.

{
  "event_type": "message.undelivered",
  "id": "MSG-77af0012",
  "phone_number": "+15551234567",
  "contact_name": "Jane Doe",
  "timestamp": "2026-09-08T14:32:10+00:00"
}

reminder.due

Fires when a scheduled conversation reminder comes due.

{
  "event_type": "reminder.due",
  "id": "CONV-3391af02",
  "phone_number": "+15551234567",
  "contact_name": "Jane Doe",
  "timestamp": "2026-09-08T14:32:10+00:00"
}

widgetsubmission.created

Fires when a visitor submits your embedded website widget.

{
  "event_type": "widgetsubmission.created",
  "id": "TWS-88ab210f",
  "widget_id": "TW-4471",
  "name": "Jane Doe",
  "phone_number": "+15551234567",
  "message": "Hi, I'd like to schedule a call.",
  "source_domain": "janedoefirm.com",
  "inbox_number": "+15559876543",
  "timestamp": "2026-09-08T14:32:10+00:00"
}

Except where noted above (survey.submitted), timestamps are ISO 8601 in UTC (+00:00 offset). Every field is always present, even if it's an empty string.

Verifying a delivery came from us

Every request we send carries two headers:

X-CC-Signaturea hex-encoded HMAC-SHA256 signature
X-CC-Timestampthe Unix timestamp (seconds) the signature was generated at

To verify a delivery, recompute the signature yourself and compare it to the one we sent — never trust a payload whose signature doesn't match, and never skip this check just because the request came in over HTTPS.

The recipe:

  1. Reject the request if either X-CC-Signature or X-CC-Timestamp is missing or empty — treat an absent header exactly like a wrong one, never as "unsigned, so skip the check".
  2. Read the raw request body exactly as it arrived — don't parse it into JSON and re-serialize it first. Even a harmless-looking change (key order, whitespace) produces a different signature than the one we computed, and the check will fail even though the payload is genuine.
  3. Build the string "{timestamp}.{raw_body}" (the timestamp from X-CC-Timestamp, a literal period, then the raw body).
  4. Compute HMAC-SHA256 of that string, keyed with your endpoint's signing secret, and hex-encode the digest.
  5. Compare it to X-CC-Signature using a constant-time comparison — a plain == leaks timing information an attacker can use to guess the signature byte by byte.
  6. We also recommend rejecting anything where X-CC-Timestamp is more than a few minutes old (we use 5 minutes on our own inbound webhooks) — that protects you against someone replaying an old, legitimately-signed request.

Worked example (Python):

import hashlib
import hmac
import time

WEBHOOK_SIGNING_SECRET = "your-endpoint-secret-here"
TOLERANCE_SECONDS = 300  # 5 minutes


def verify_currentclient_signature(
    raw_body: bytes, timestamp_header: str, signature_header: str
) -> bool:
    """Verify an X-CC-Signature / X-CC-Timestamp pair against raw_body.

    raw_body must be the exact, unparsed request body - read it before
    any JSON deserialization happens.
    """
    try:
        timestamp = int(timestamp_header)
    except (TypeError, ValueError):
        return False

    if abs(int(time.time()) - timestamp) > TOLERANCE_SECONDS:
        return False

    signed_payload = f"{timestamp}.".encode("utf-8") + raw_body
    expected_signature = hmac.new(
        WEBHOOK_SIGNING_SECRET.encode("utf-8"), signed_payload, hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(expected_signature, signature_header)


# Example use in a Flask-style view:
#
#   raw_body = request.get_data()  # bytes, unparsed
#   ok = verify_currentclient_signature(
#       raw_body,
#       request.headers.get("X-CC-Timestamp", ""),
#       request.headers.get("X-CC-Signature", ""),
#   )
#   if not ok:
#       abort(401)

Retries, delivery logs, and what happens when your endpoint goes quiet

If your endpoint returns anything other than a 2xx status, or doesn't respond at all, we'll retry with exponential backoff: 30 seconds, 1 minute, 2 minutes, 4 minutes, 8 minutes, 16 minutes, 32 minutes, and a final retry about an hour after that — 8 retries in all, after which that event's schedule is exhausted.

If three separate events in a row each burn through their entire retry schedule with no successful delivery, we automatically disable the endpoint — that way a broken integration doesn't just fail silently and pile up. When that happens:

Fixing it: once you've sorted out whatever was wrong at your end, go back to Settings > Developers > Webhooks, find the endpoint, and click Re-enable. That clears the disabled state and resets the failure count so new events start delivering again right away. One thing it deliberately does not do: replay whatever was missed while the endpoint was disabled — if that gap matters to you, reconcile it from your own records, or from our API, once you're back up.

Every attempt — success or failure — lands in that endpoint's delivery log, along with the event, timestamp, and the response we got back, so you can see exactly what went wrong before you need to hit Re-enable.

Rotating your signing secret

If your secret ever leaks, or you just want to cycle it as good hygiene, you can rotate it from the same endpoint card. Rotating takes effect immediately — the moment you do it, deliveries start signing with the new secret, so update your verification code with the new value right away. Anything you haven't updated will start failing its signature check until you do.

Common questions

Are deliveries signed, and do I have to verify them?

Yes — every delivery from an endpoint that has a signing secret (every endpoint created in Settings > Developers > Webhooks does) carries an X-CC-Signature header. Verifying it is optional on our side (we don't reject you for not checking) but strongly recommended.

Where do I get the signing secret?

Each endpoint has its own secret, generated by us (a 64-character hex string) — you can't choose it. Find it under Settings > Developers > Webhooks > your endpoint > Reveal. It's also returned when you create the endpoint or rotate its secret.

Quick reference: header, algorithm, and what's signed

Signature headerX-CC-Signature
AlgorithmHMAC-SHA256, hex-encoded (no sha256= prefix)
KeyThe endpoint's signing secret, used as its UTF-8 string (not hex-decoded)
Signed value"{timestamp}.{raw_body}" — the timestamp, a literal period, then the exact raw request body
Timestamp headerX-CC-Timestamp — Unix time in whole seconds (e.g. 1789000000)

Does creating or re-enabling an endpoint change the secret?

Creating an endpoint generates its secret, once. Editing an endpoint, changing its subscribed events, or clicking Re-enable never changes it. Only an explicit Rotate secret does — and that takes effect immediately.

Does reconnecting an OAuth integration affect my webhook?

No. A webhook endpoint is its own record with its own secret, separate from any OAuth-connected integration. Reconnecting or re-authorizing an OAuth integration doesn't create, delete, or rotate a webhook endpoint or its signing secret.