Couplr Couplr developers
API v1 · explained by Flip

Inbound webhooks

Give any app a URL that turns what it sends into Couplr customers or jobs. Covers setup, sender authentication, field mapping, acknowledgments, duplicates, retries and troubleshooting.

View Markdown

An inbound webhook is a URL that belongs to one Couplr company. Another system sends data to it with an HTTP POST. Couplr stores what arrived, then uses a field mapping you set up to create or update a customer, or to create a job.

Use one when the other system can send data to a URL but cannot call an API, for example a website form, an ad lead form routed through Zapier, or a scheduling tool.

Set up an endpoint#

A company admin does this on the Integrations page (https://couplrfsm.com/dashboard/integrations), in the "Receive data" section.

  1. Choose New webhook and give it a name, such as "Website contact form".
  2. Leave Require a signature (advanced) off unless the sending app can sign its requests. See Sender authentication.
  3. Copy the webhook URL. It looks like https://couplrfsm.com/hooks/YOUR_WEBHOOK_TOKEN. If you turned signatures on, also copy the signing secret. It is shown once.
  4. Get a sample: have the sending app send one real submission to the URL, or paste an example payload.
  5. Review the field mapping Couplr proposes, adjust it and save. See Field mapping.
  6. Turn the webhook on. Its status changes from "Waiting for a sample" to "Active".

The URL contains a long random token that identifies the endpoint and the company. Treat the whole URL as a secret. The company is always taken from the endpoint. A company or tenant id inside the payload has no effect.

Send data#

Send a POST to the webhook URL with one of:

  • A JSON object: Content-Type: application/json.
  • A JSON array of objects. Each element is treated as a separate event. At most 25 per request.
  • A form post: application/x-www-form-urlencoded, including nested keys such as contact[email], or multipart/form-data fields. Uploaded files are ignored. A signed webhook (hmac or standard_webhooks) can't verify multipart/form-data and answers 415; send JSON or a urlencoded form instead.
curl -X POST "https://couplrfsm.com/hooks/YOUR_WEBHOOK_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Event-Id: lead-8841" \
  -d '{"name": "Dana Whitfield", "email": "dana@example.com", "phone": "555-201-8890"}'

Optional headers:

Header Purpose
X-Event-Id Your unique id for this event, up to 191 characters. Used to recognize duplicates. Idempotency-Key is accepted as another name for it.
X-Event-Type A label for the kind of event, up to 100 characters. Stored with the event.

Limits:

Limit Value Response when exceeded
Request body size 256 KB 413
Events in one JSON array 25 413
Requests per endpoint 120 a minute 429

Couplr never fetches URLs, downloads attachments or runs anything found in a payload. Links and file references arrive as plain text.

Sender authentication#

An endpoint works in one of two ways, chosen when it is created.

Secret URL (default). The URL is the only credential. Anyone who has the URL can send data to it. This is the only choice for senders that cannot add headers, and it is what most form tools and automation steps use. It proves the sender knows the URL. It does not prove who sent the request or that the body was not changed in transit by something that also knows the URL. If the URL may have leaked, delete the webhook and create a new one.

Signature required. With "Require a signature" on, every request must carry an X-Hvacpro-Signature header holding the hex HMAC-SHA256 of the request, computed with the signing secret. A request with a missing or wrong signature gets 401 and is not stored or processed. There is no fallback to the secret URL.

Sign one of two ways:

  1. Recommended. Also send X-Hvacpro-Timestamp with the current Unix time in seconds, and sign the string TIMESTAMP.BODY, where BODY is the exact bytes you send. The timestamp must be within 5 minutes of the current time, so a captured request cannot be replayed later.
  2. Without the timestamp header, sign the exact body bytes alone.
BODY='{"name":"Dana Whitfield","email":"dana@example.com"}'
TS=$(date +%s)
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "YOUR_SIGNING_SECRET" -hex | sed 's/^.* //')
curl -X POST "https://couplrfsm.com/hooks/YOUR_WEBHOOK_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Hvacpro-Timestamp: $TS" \
  -H "X-Hvacpro-Signature: $SIG" \
  -d "$BODY"

The signature may be prefixed with sha256=. Sign the bytes you actually send: re-serializing the JSON after signing changes the bytes and the signature will not match.

Test events#

  • While a webhook is "Waiting for a sample", the first request it receives is saved as the sample used to build the mapping. Nothing is created in Couplr.
  • You can paste an example payload instead of sending one.
  • Requests received before the webhook is turned on are stored with status "Skipped" and create nothing. After you turn the webhook on you can replay a skipped event from its activity list.

There is no separate test mode in the Integrations screens yet that previews what a live event would create. To try a mapping safely, send a test submission with a contact you recognize, check the result in the activity list, then delete the test customer.

Field mapping#

A mapping says which record to create and where each of its fields comes from. Couplr proposes one from your sample. You can edit every row.

Records:

Record What happens
Customer Creates a customer, or updates the existing one that matches.
Job Does the same for the customer, then creates a job for them. Needs a job type that exists in your account, matched by name.

Customer fields: name, email, phone, address, city, state, zip_code. Job fields: scheduled_date, notes.

Each field takes its value one of three ways:

Choice Meaning Example
Copy a field The value at a path in the payload. Use dots for nested data. contact.email
Combine fields Text with {{path}} placeholders. {{first_name}} {{last_name}}
Always this value The same fixed value every time. Website

Matching an existing customer: Couplr looks for a customer in your company with the same email, then the same phone number. A phone number only matches when it is a full 10-digit number, with or without a leading 1; shorter values never match. When one is found, the mapping's "When a matching customer already exists" setting decides whether to update their details or skip the event.

Other options:

  • Start a Speed to Lead text conversation with new leads. Texts the homeowner as soon as a new customer is created. It is off unless you turn it on, and it never runs for a customer who already existed.
  • Mapped values go through the same validation as the Couplr apps. An event whose values are invalid, such as a malformed email, fails with the reason shown. See Processing status.

Acknowledgments#

Couplr answers as soon as the event is safely stored. The answer means "received", not "processed": the customer or job is created a moment later in the background.

Response Body Meaning
202 {"received": true} Stored. Processing is queued.
200 {"received": true, "duplicate": true} Already received. Not processed again.
202 {"received": true, "events": 3, "duplicates": 0} A JSON array was received: number of events and how many were duplicates.
401 {"message": "Invalid signature."} Signature missing, wrong or too old. Nothing was stored.
404 {"message": "Unknown webhook."} No such URL. It may have been deleted.
413 {"message": "Payload too large."} Over 256 KB, or more than 25 events in one array.
415 {"message": "Unsupported content type. Send JSON or a form post."} The body is not JSON or a form post.
422 {"message": "Empty or unparseable payload."} Empty body, invalid JSON, or an array element that is not an object.
429 {"message": "Too many requests."} Over 120 requests a minute for this endpoint.

Treat any 2xx as success. On 429 and 5xx, send the same request again later. Do not retry other 4xx responses without changing the request.

Processing status#

Each event appears in the webhook's "Recent activity" list on the Integrations page with one of these statuses:

Status Meaning
Received Stored and waiting to be processed, or being processed.
Processed The mapping ran. The entry shows what was created or updated.
Failed The mapping could not be applied. The entry shows why, for example a missing name or an invalid email.
Skipped Received while the webhook was not active. Nothing was created.
Duplicate The same event had already been received. Nothing was created.

A failed or skipped event can be replayed from the list once the webhook is active. A replay runs the event again with the current mapping, so the usual fix for a failed event is: correct the mapping, save, replay.

Stored events are kept for 90 days, then deleted. Deleting a webhook deletes its stored events.

Duplicates#

Senders retry, so the same event can arrive more than once. Couplr recognizes a repeat in two ways:

  • By event id. When you send X-Event-Id, a second request with the same id to the same endpoint is a duplicate, whatever its body. Ids are case-sensitive and belong to one endpoint. This is the reliable method. Use it when you can.
  • By body. Without an event id, a request whose body is byte-for-byte identical to one received on the same endpoint in the last 24 hours is a duplicate. Two different people submitting identical data would also match, and a retry that re-serializes its JSON differently would not.

On a webhook that requires a signature, both checks apply.

Processing is safe to repeat: a customer is matched and updated instead of created twice, and a job is tied to the event that created it, so replaying that event does not book a second job.

Retries#

  • Sending. If you get no response, a 429 or a 5xx, send the request again. Use the same X-Event-Id so a request that did arrive is not processed twice.
  • Processing. If processing fails for a temporary reason, Couplr tries again, up to 3 attempts in total, then marks the event "Failed". Invalid data fails at once without further attempts.
  • Order. Events may be processed in a different order from the order they were sent. Do not rely on order.

Troubleshooting#

Symptom Likely cause Fix
401 Invalid signature Wrong secret, or the body changed after signing, or the clock is more than 5 minutes off. Sign the exact bytes sent. Check the secret and the system clock.
404 Unknown webhook The URL is mistyped or the webhook was deleted. Copy the URL again from the Integrations page.
415 The sender is posting XML, CSV or plain text. Send JSON or a form post.
422 Empty or unparseable payload Empty body or invalid JSON. Check the body the sender produces.
200 with duplicate: true The same event id, or an identical body, was already received. Expected for retries. For a genuinely new event, send a new X-Event-Id.
Status stays "Waiting for a sample" No request has reached the URL yet. Send a test submission, or paste a sample.
Events show "Skipped" The webhook is not turned on. Save a mapping, turn the webhook on, then replay the skipped events.
Event "Failed" with a validation message A mapped value is missing or invalid. Fix the mapping or the sender's data, then replay.
Job events fail on job type The mapping's job type does not match a job type name in your account. Use an exact job type name.

Not available yet#

These are built into the service but cannot be set from the Integrations screens today, so do not plan on them:

  • Bearer-token authentication for senders, and verification of Standard Webhooks signatures.
  • Replacing a signing secret or a webhook URL in place. Delete the webhook and create a new one instead.
  • A preview mode that shows what an event would create without creating it.
  • Default values, required fields and type conversion in mappings, and matching customers by the sender's own id.

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.