An outbound webhook sends an HTTP POST to a URL you choose each time a chosen event happens in the company's Couplr account.
Set up a destination#
A company admin does this on the Integrations page (https://couplrfsm.com/dashboard/integrations).
- Add a new destination and enter the URL. It must start with
https://and use a public hostname. IP addresses and private or local hosts are refused. - Choose the events to send, or all events.
- Copy the signing secret when it is shown. It starts with
whsec_and is shown once. - Use the test button to send a test delivery and confirm your endpoint receives it.
An app can also set up its own destination through the API, with a key that has the webhooks:manage ability. See Create a webhook subscription. The signing secret comes back in that response, once. A subscription created this way belongs to the key that created it: once that key is revoked or expires, it gets no more deliveries. The Integrations page shows which key created each one.
Events#
| Event | Sent when |
|---|---|
customer.created |
A customer is created. |
job.created |
A job is created. |
job.completed |
A job is completed. |
estimate.completed |
An estimate visit is completed. |
invoice.sent |
An invoice is sent to the customer. |
invoice.paid |
An invoice is paid. |
invoice.refunded |
An invoice is refunded in full. |
estimate.accepted |
A customer accepts an estimate. |
lead.created |
A new lead conversation starts. |
booking.created |
A customer books a visit themselves. |
Payload#
Each delivery is a POST with a JSON body:
{
"id": "evt_48213",
"event": "customer.created",
"created_at": "2026-11-14T15:02:11+00:00",
"data": {
"id": 1842,
"name": "Dana Whitfield",
"email": "dana@example.com",
"phone": "5552018890"
}
}
| Field | Meaning |
|---|---|
id |
Unique id of this delivery. The same value on every retry of it. |
event |
The event name. A test delivery uses test. |
created_at |
When the event happened, ISO 8601 in UTC. |
data |
A short summary of the record: its id and a few key fields. Fetch the full record from the API using the id. |
Request headers:
| Header | Value |
|---|---|
Content-Type |
application/json |
User-Agent |
HVACPro-Webhooks/1.0 |
X-Hvacpro-Event |
The event name. |
X-Hvacpro-Signature |
Hex HMAC-SHA256 of the raw request body, using the destination's signing secret. |
Verify the signature#
Compute the HMAC-SHA256 of the raw request body with your signing secret and compare it with X-Hvacpro-Signature using a constant-time comparison. Do this before parsing the JSON, and use the raw bytes, not a re-serialized copy.
const crypto = require('crypto');
function isFromCouplr(rawBody, signatureHeader, signingSecret) {
const expected = crypto.createHmac('sha256', signingSecret).update(rawBody).digest('hex');
const given = Buffer.from(signatureHeader || '', 'utf8');
const wanted = Buffer.from(expected, 'utf8');
return given.length === wanted.length && crypto.timingSafeEqual(given, wanted);
}
The signature covers the body only. It does not include a timestamp, so it cannot tell you how old a delivery is. Use the body's id to ignore a delivery you have already handled.
Respond and retry#
- Answer with any
2xxstatus within 10 seconds to accept a delivery. Do slow work after responding. - Any other status, a timeout or a connection error counts as a failure. Couplr tries up to 4 times in total, waiting about 1, 5 and 15 minutes between attempts.
- Redirects are not followed.
- The same delivery can arrive more than once, and deliveries can arrive out of order. Deduplicate on
idand do not assume order. - After 10 deliveries in a row have used up all their attempts, the destination is turned off automatically. The Integrations page shows this. Fix your endpoint, then turn the destination back on there.
The Integrations page lists recent deliveries for each destination with their status and response code.
Not supported#
- Replacing a signing secret in place. Delete the destination and add it again to get a new secret.
- Choosing the fields inside
data. - An event for refunded invoices.