Couplr Couplr developers
API v1 · explained by Flip

Outbound webhooks

Receive a signed POST from Couplr when something happens in an account. Covers events, payload, signature verification, retries and automatic disabling.

View Markdown

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).

  1. 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.
  2. Choose the events to send, or all events.
  3. Copy the signing secret when it is shown. It starts with whsec_ and is shown once.
  4. 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 2xx status 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 id and 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.

Copy this page as Markdown

Your browser blocked automatic copying. The complete page is selected below. Press Ctrl+C (Cmd+C on a Mac) to copy it.