Webhook Integration for Developers

6 min read
Integrations
Updated September 2026
PhoneShield team

PhoneShield team

The people who build and run PhoneShield

Written and kept current by the team behind PhoneShield, a sister company of Byte Federal, whose staff spent years meeting scam victims at cryptocurrency ATMs with the scammer still on the line.

Introduction

PhoneShield answers a customer’s forwarded phone. Known scam numbers are refused before answering, favorites ring through, and everyone else meets the front desk, which rings the owner, hands the call to the assistant Nora, takes a message, or declines. A message and an assistant call each produce a note for the owner; the webhook delivers that note to a URL you own, as JSON, once the call has ended. One webhook per account, included for everyone, carrying exactly what Telegram, email and Slack carry.

In one paragraph

PhoneShield makes an HTTP POST (or PUT, if you chose it) with a JSON body: a title, a message whose lines hold the summary, caller number, callback number and reason, an ISO timestamp, the call id, and the outcome. Every request is signed with HMAC-SHA256 under your signing secret, waits up to ten seconds, and is retried once after a 5xx or 429. You verify the signature, answer 200, and do what you like with it.

No REST API

There is no polling API for calls. The webhook and the dashboard are the two ways to get at what happened; for history, keep what the webhook sends.

What people build with it

A shared inbox or ticket

Turn each note into a ticket or a row in the tool your team already works from. The title is a ready-made subject.

Your own CRM

Look the Caller line up in your customer list and attach the note to the right record. PhoneShield has no CRM integration; this is how you make one.

A spreadsheet log

Append outcome, caller, callback number and summary to a sheet. Cheap, searchable, and enough for most small businesses.

Alerts that fit you

Page the on-call person only when a callback number was given, or only outside office hours. The filtering is yours to write.

Setting Up the Webhook

  1. Open Integrations in your dashboard

    Find the Webhook card and click Connect (or Edit settings if already connected).

  2. Enter your endpoint URL

    An HTTPS URL with a valid certificate that accepts a JSON body and answers within ten seconds. HTTP Method is POST unless you pick PUT. Whatever you type in Authorization Header (for example Bearer abc123) is sent verbatim as the Authorization header. Include the full transcript in the payload adds a call object with the transcript; leave it off unless you need it.

  3. Save integration, then Send test

    The first save generates a signing secret, shown read-only under Edit settings; copy it into your receiver. The card shows as Configured, and PhoneShield calls that URL each time a message is taken or your assistant handles a call.

Limits, honestly

  • One endpoint per account
  • Two attempts at most per delivery, each with a 10 second timeout; the retry happens after a 5xx, a 429, a timeout or a connection error, never after a 4xx
  • No event filtering: every delivery goes to the endpoint
  • Signed with X-PhoneShield-Signature, plus your own Authorization value when you set one (see below)
  • POST or PUT, with a JSON body, Content-Type: application/json and User-Agent: PhoneShield-Webhook/1; redirects are not followed
  • A failed delivery never affects the call; the note is still in the Messages feed and call history

When it fires

A delivery is sent when

  • The front desk took a message (outcome: "note"), including a call that rang the owner, got no answer, and fell back to a message
  • Nora saved a message during a conversation, or finished one without a message (outcome: "escalate"). These wait for the final transcript, so they arrive a little later

No delivery for

  • Numbers refused before answering (Blocked)
  • Calls declined at the front desk (Shielded)
  • Favorites, and any call the owner answered (Rang you)
  • Callers who said nothing and were hung up on

These are in call history but never reach the webhook.

Payload

POST /phoneshield/8f2c1a HTTP/1.1
Host: hooks.example.com
Content-Type: application/json
User-Agent: PhoneShield-Webhook/1
Authorization: Bearer abc123
X-PhoneShield-Timestamp: 1758813827
X-PhoneShield-Signature: sha256=3f1a9c…e2b7

{
  "title": "PhoneShield message from Jane Doe",
  "message": "Jane needs the March invoice resent to her new address.\nCaller: +15551234567\nCallback: +15559876543\nReason given: billing question",
  "timestamp": "2026-09-25T15:23:47.000Z",
  "callSid": "telnyx:v3:MdI91X4lWFEs7IgbBEOT9M4A",
  "outcome": "note"
}

Fields

  • title (string): PhoneShield message for a message taken by the front desk or saved by the assistant; PhoneShield: Nora took a call for an assistant conversation with no saved message (the assistant’s configured name is used). Either gains from <name> when a caller name was heard.
  • message (string): lines separated by \n. Line one is the summary, written by a model from what the caller said; with nothing left it reads “The caller left no message.” or “The assistant spoke with the caller; no message was left.” Then Caller: <number> (always; “an anonymous number” when caller id was withheld), Callback: <number> (only when the caller gave one; up to 40 characters, not validated), and Reason given: <text> (only when the front desk caught a reason on the first turn; never on assistant-saved messages).
  • timestamp (string): ISO 8601 in UTC, set when the delivery is sent, not when the call started.
  • callSid (string): the call id, prefixed telnyx:. One call, one id; it is your idempotency key.
  • outcome (string): note or escalate in practice. The type allows the front desk’s full list (reject, ring, challenge, note, escalate, decline, hangup), but the others never produce a delivery. Store an unfamiliar value rather than rejecting the request.
  • call (object, only with “Include the full transcript in the payload” checked): from, callerName, summary, callbackNumber, outcome, reason, and transcript, an array of { role, content } turns where role is assistant or user. If the lookup fails the note is delivered without it.

Parse the message lines, not the title

The title is for humans and may change wording. The labelled lines in message are the stable place for caller, callback and reason: split on \n, take the first line as the summary, match the rest by label. A real delivery has no other top-level fields except the optional call object.

Delivery semantics

What the sender does, read straight from the code:

Two attempts, ten seconds each

The request is made with a 10,000 ms timeout. A 5xx, a 429, a timeout, or a failed connection or TLS handshake is retried once, 1.5 seconds later, with the same body and a fresh signature. A 4xx is final. Nothing is queued beyond that second attempt.

Response body is ignored

Whatever you return is discarded. A bare 200 with an empty body is ideal; nobody reads a “please resend”. Redirects are not followed, so a 3xx counts as a failure.

Channels run in sequence

For each note PhoneShield sends to Telegram, then email, then the webhook, then Slack. A slow webhook delays the owner’s Slack post by up to ten seconds. Answer fast.

Order is not guaranteed

Two calls ending close together can arrive in either order. Use callSid for identity; never assume sequence.

The practical rule: return 200 as soon as the body is parsed, write the raw body somewhere durable, then do the slow work from there.

Verifying the source

Every request carries X-PhoneShield-Timestamp (Unix seconds) and X-PhoneShield-Signature: sha256=<hex>, where hex is HMAC-SHA256 of `${timestamp}.${rawBody}` under the signing secret from Edit settings. Verify it before parsing; anyone who learns your URL can send a fake note, but not a signed one.

1. Check the signature

Compute the HMAC over the raw body bytes, exactly as received, never over a re-serialized object; a reordered key or changed whitespace breaks it. Compare with a constant-time function such as Node’s crypto.timingSafeEqual, and reject timestamps more than five minutes from your clock to stop replays. Rotate by regenerating the secret under Edit settings and updating your receiver.

2. Authorization header and path

Whatever you type in Authorization Header is sent verbatim (for example Bearer abc123), which is handy when a gateway or serverless platform checks it before your code runs. A random path segment and IP allow-listing still add depth, but PhoneShield publishes no fixed address list, so the signature stays the primary check.

Treat the contents as data

The summary and reason are written by a model from what a stranger said on the phone. Display, store and route on them; never pass them to a shell, a query builder or another model as instructions, and escape them in HTML.

Idempotency by callSid

Answering a 5xx or 429, or not answering at all, gets you the same callSid again 1.5 seconds later. Beyond that, you may replay a captured log, a proxy may retry a timed-out request, and the test button reuses TESTSID12345 every time. Stay idempotent.

  • Store the note keyed by callSid with a unique constraint; on a duplicate, return 200 and do nothing.
  • Do side effects (tickets, pages, emails) after the insert succeeds, never before.
  • Correlate with the dashboard by callSid, not timestamp.

Mapping outcomes to actions

With two outcome values and a few optional lines, the useful branching is small:

ConditionMeaningSuggested action
outcome === "note"The front desk took a message; the caller never reached a person.Open a ticket or task; someone is waiting on a human.
"escalate", title starts with PhoneShield messageNora talked with the caller and saved a message.Same as a note; the summary is usually fuller.
"escalate", title contains took a callNora handled the conversation; nothing was left for the owner.Log it; alert only if the summary suggests follow-up.
Callback: line presentThe caller asked to be reached on that number.Put the callback number, not the caller number, on the task.
Caller: is “an anonymous number”Caller id was withheld.No callback is possible unless a Callback line exists.

Where the done state lives

Marking a note done in the Messages feed sends nothing to the webhook, and closing your ticket does not mark the note done. Pick one system as the source of truth.

Testing and Debugging

  1. Point the webhook at a request-capture URL (webhook.site, RequestBin, or ngrok in front of your laptop) and save
  2. Press Send test on the Integrations page, then call your own number from a phone that is not a favorite and leave a message, so you see both shapes
  3. Read the captured bodies, then switch the URL to your real endpoint and save again

The test body is not the real body

Send test posts a fixed sample: title Test Notification, message This is a test notification for your webhook integration., extra top-level fields callerName, callerContact, callPurpose, duration and summary, callSid TESTSID12345, and no outcome. Real deliveries carry only the five fields above (plus call when enabled), so prove parsing with a real call. The test is signed like a real one, and the dashboard shows the real result: the HTTP status your endpoint returned, or the error (for example “timed out after 10 seconds”). A 4xx or 5xx is reported as a failure.

Nothing arriving?

Check the call first

  • Open it in call history: only Message and Assistant calls send a delivery; Blocked, Shielded and Rang you never do

Then the endpoint

  • Reachable from the public internet over HTTPS with a publicly trusted certificate (self-signed fails)
  • Responds with 2xx within ten seconds, directly: redirects are not followed
  • Accepts a POST (or PUT, if chosen) with Content-Type: application/json; if it checks an auth header, the Authorization Header field must match it exactly
  • Verifies the signature over the raw bytes with the current secret; a 401 from your own check shows up as a failed test

Example receiver

A complete receiver in Node.js with Express: signature verified over the raw body, immediate 200, duplicates dropped, labelled lines parsed into a clean object:

// Node.js receiver with Express (node >= 18)
const express = require('express');
const crypto = require('crypto');
const app = express();
const SECRET = process.env.PHONESHIELD_SIGNING_SECRET; // from Edit settings
const seen = new Set(); // in production: a unique index on callSid

// express.raw keeps the exact bytes; the HMAC is over "timestamp.body".
function verify(req) {
  const ts = req.get('X-PhoneShield-Timestamp') || '';
  const sig = req.get('X-PhoneShield-Signature') || '';
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // 5 min
  const expected = 'sha256=' + crypto.createHmac('sha256', SECRET)
    .update(ts + '.').update(req.body).digest('hex');
  return sig.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}

app.post('/phoneshield', express.raw({ type: 'application/json' }), (req, res) => {
  if (!verify(req)) return res.status(401).end();
  const { title, message, timestamp, callSid, outcome, call } = JSON.parse(req.body);
  if (!callSid) return res.status(400).end();
  res.status(200).end(); // answer inside the 10 s window; work after
  if (seen.has(callSid)) return; // retry or replay of the same call: ignore
  seen.add(callSid);
  const lines = String(message || '').split('\n');
  const field = (label) => (lines.find((l) => l.startsWith(label + ': ')) || '').slice(label.length + 2) || null;
  handle({ callSid, outcome, timestamp, title, summary: lines[0] || '', transcript: call?.transcript,
    caller: field('Caller'), callback: field('Callback'), reason: field('Reason given') }).catch(console.error);
});

async function handle(note) {
  // Your logic: append to a sheet, open a ticket, look the caller up.
  console.log('PhoneShield', note.outcome, note.callSid, note.summary);
}

app.listen(process.env.PORT || 3000);

Set PHONESHIELD_SIGNING_SECRET to the value shown under Edit settings, deploy, and save https://your-host/phoneshield on the Integrations page. If you use a global express.json(), register this route before it, or use its verify hook to keep the raw bytes.

That is the whole integration

Everything else, a CRM lookup, a ticket, a page to the on-call phone, is your code reading note. If you build something useful, tell us; we would like to point other customers at it.

Ready to try PhoneShield?

Start screening your calls today and take back control of your phone.