Call Event Webhooks
Breesy can push voice-agent call events, inbound texts, and Customer Portal messages to your systems as they happen, so you don’t need to poll the Data Share API on a schedule. Each webhook subscription is franchise-scoped, delivers signed JSON payloads over HTTPS, and retries automatically for up to 24 hours.
Subscriptions
A subscription is an HTTPS endpoint plus the set of events it should receive. Subscriptions are managed by your Breesy administrator (Breesy admin or franchise Power Admin) from within Breesy. Each subscription has:
| Field | Description |
|---|---|
url | Your HTTPS endpoint. Breesy sends a POST request per event. |
events | The event types to deliver (defaults to all). |
secret | A signing secret generated when the subscription is created. It is shown once — store it securely. |
active | Inactive subscriptions receive no deliveries. |
No-code tools work too: paste the webhook URL that Zapier or Make gives you, and every completed call arrives as a ready-to-use JSON payload.
Event Types
| Event | Fires when |
|---|---|
call.completed | A voice-agent call has ended and post-call processing has finished |
sms.received | A customer texted one of your Breesy Phone numbers and the text was saved in Breesy |
portal.message_received | A customer sent a message from their Customer Portal (the Messages tab of their secure portal link) |
call.completed fires once per call, as soon as post-call processing finishes — when the transcript, summary, categorization, and recording link are all available. A single delivery carries everything, so no follow-up fetch is needed.
sms.received fires once per inbound text, as soon as Breesy saves it. It covers texts that appear in Breesy OS: texts to numbers in an SMS group, replies from customers you already have a text thread with, and group texts. Texts from numbers on your Spam Handling list are not delivered.
portal.message_received fires once per customer message, as soon as the customer sends it from the portal. Replies your team sends from Breesy OS and automatic system notes in the portal thread are not delivered. Use it to surface portal messages in your own tools, or to text or email your team when a customer writes in.
Request Format
Every delivery is a POST with Content-Type: application/json and these headers:
| Header | Description |
|---|---|
X-Breesy-Event | The event type, e.g. call.completed |
X-Breesy-Delivery-Id | Unique ID for this delivery. The same ID is reused on retries — use it for idempotency. |
X-Breesy-Signature | sha256=<hex> HMAC signature of the raw request body |
Example Payload
{ "event": "call.completed", "delivery_id": "7f0d3c62-1111-2222-3333-444455556666", "occurred_at": "2026-06-22T18:04:11.000Z", "conversation_id": "CA0123456789abcdef0123456789abcdef", "franchise_id": "0c8b2a1e-1111-2222-3333-444455556666", "call": { "conversation_id": "CA0123456789abcdef0123456789abcdef", "caller_id": "+15551234567", "timestamp": "2026-06-22T18:04:11+00:00", "duration": "182", "agent_type": "afterhours", "call_category": "new_urgent_service_request", "customer_name": "Jordan Smith", "customer_email": "jordan@example.com", "call_summary": "Water damage in the basement after a pipe burst.", "loss_type": "Water", "referral_source": "Google", "notes": "Caller requested a morning callback.", "property_area": "Basement", "request_type": "INITIAL_SERVICE_REQUEST", "service_location": "123 Main St, Springfield", "callback_number": "+15557654321", "location_id": "aaaa1111-2222-3333-4444-555566667777", "location": "North Branch", "transcript": "Agent: Thanks for calling...", "real_estate": { "address": "123 Main St, Springfield", "last_sale_value": "415000", "current_value": "468000", "owner_name1": "Jordan Smith", "owner_name2": null, "room_count": "7", "square_footage": "2100", "year_built": "1998" }, "recording_url": "https://api.breesy.app/data/recordings/CA0123456789abcdef0123456789abcdef", "recording_breesy_url": "https://www.breesy.app/insights?tab=calldata&callId=conv_CA0123456789abcdef0123456789abcdef" }}Example sms.received Payload
{ "event": "sms.received", "delivery_id": "9a1e5b20-1111-2222-3333-444455556666", "occurred_at": "2026-06-22T18:10:42.000Z", "franchise_id": "0c8b2a1e-1111-2222-3333-444455556666", "sms": { "id": "5d2c7f10-1111-2222-3333-444455556666", "received_at": "2026-06-22T18:10:42.000Z", "from": "+15551234567", "to": "+15559876543", "body": "Can someone come out tomorrow morning?", "customer_id": 48213, "location_id": "aaaa1111-2222-3333-4444-555566667777", "location": "North Branch", "group_session_id": null, "group_participants": null, "attachments": [] }}from is the customer’s number and to is your Breesy Phone number. customer_id is the matching Breesy customer, or null if none was found. For group texts, group_session_id identifies the thread and group_participants lists every outside number on it. attachments lists the content type and filename of any pictures or files; the files themselves are viewable in Breesy OS.
Example portal.message_received Payload
{ "event": "portal.message_received", "delivery_id": "c4b7e9d1-1111-2222-3333-444455556666", "occurred_at": "2026-06-22T18:15:03.000Z", "franchise_id": "0c8b2a1e-1111-2222-3333-444455556666", "message": { "id": "2e8f4a61-1111-2222-3333-444455556666", "received_at": "2026-06-22T18:15:03.000Z", "body": "Hi, just checking in. Is the crew still coming out tomorrow morning?", "sender_name": "Jordan Rivera", "portal_link_id": "7b3d9c10-1111-2222-3333-444455556666", "customer": { "name": "Jordan Rivera", "phone": "+15550142233", "email": "jordan.rivera@example.com", "service_address": "418 Maple Court, Springfield, IL 62704" }, "customer_id": 48213, "service_request_id": 10492, "breesy_job_id": null, "dash_job_id": null, "location_id": "aaaa1111-2222-3333-4444-555566667777", "location": "North Branch" }}body is the text the customer typed. customer holds the details the customer confirmed when they opened the portal (name and phone are always present; email and service_address are null if not collected). portal_link_id identifies the customer’s portal, so every message from the same customer shares it — use it to group a conversation. Exactly one of service_request_id, breesy_job_id, or dash_job_id is set when the portal is attached to a call or job; customer_id is the matching Breesy customer, or null. Reply to the customer from Breesy OS; the webhook is one-way.
The call object uses the same shape as the GET /data/calls endpoint. It reflects the call’s state at send time, not at the moment the event occurred, so a retried delivery may contain newer call data than the original attempt. recording_url is the stable Breesy recording link — it never expires from your side and requires your Data Share API key when fetched.
Verifying Signatures
Each request body is signed with HMAC SHA-256 using your subscription’s secret. Always verify the signature before trusting a payload.
import crypto from "node:crypto";
function verifyBreesySignature(rawBody, signatureHeader, secret) { const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex"); return crypto.timingSafeEqual( Buffer.from(signatureHeader), Buffer.from(expected), );}
// Express example — use the raw (unparsed) request bodyapp.post("/webhooks/breesy", express.raw({ type: "application/json" }), (req, res) => { if (!verifyBreesySignature(req.body, req.get("X-Breesy-Signature"), process.env.BREESY_WEBHOOK_SECRET)) { return res.status(401).send("invalid signature"); } const event = JSON.parse(req.body); // ... process event ... res.status(200).send("ok");});Compute the HMAC over the raw request body bytes — re-serializing the parsed JSON can change key ordering or whitespace and produce a different signature.
Responding to Deliveries
Respond with any 2xx status within 10 seconds to acknowledge a delivery. Anything else (non-2xx, timeout, connection error) counts as a failed attempt and the delivery is retried.
Process asynchronously if your handling is slow: acknowledge immediately, then do the work.
Retry Schedule
Failed deliveries are retried with exponential backoff — up to 8 total attempts, with minimum delays between attempts:
| Attempt | Minimum delay after previous failure |
|---|---|
| 1 | Immediately after post-call processing finishes (or, for sms.received / portal.message_received, when the text or message is saved) |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 15 minutes |
| 5 | 1 hour |
| 6 | 3 hours |
| 7 | 6 hours |
| 8 | 12 hours |
Dispatch is event-driven: a retry that has become due is sent the next time the dispatcher runs (triggered by any subsequent call event, or by a manual redelivery initiated by a Breesy administrator), so the actual delay can be longer than the minimum during quiet periods.
After the final attempt fails, the delivery is marked failed and will not be retried automatically. Your Breesy administrator can view recent deliveries (including failures and the last error) and delivery success-rate stats inside Breesy.
Delivery Guarantees
- At-least-once delivery. A delivery may occasionally be sent more than once (for example, if your endpoint responds slowly and the acknowledgement is missed). Deduplicate using
X-Breesy-Delivery-Id. - No strict ordering. Retries can cause a later call’s event to arrive before an earlier one. Use the call’s
timestamprather than arrival order. - Reconciliation. Webhooks are best-effort by nature. If you need a guaranteed complete record, periodically reconcile against
GET /data/callsusing a date range.