# Integration examples

> Working recipes for connecting Zapier, Make, n8n, website forms and plain HTTP clients to Couplr.

- Canonical URL: https://couplrfsm.com/developers/examples
- Markdown: https://couplrfsm.com/developers/examples.md
- API version: v1 (base URL https://couplrfsm.com/api/v1)
- OpenAPI specification: https://couplrfsm.com/developers/openapi.yaml
- All documentation pages: https://couplrfsm.com/llms.txt

> **Flip:** Here are the connections people ask me for most, written as recipes you can copy. I don't have my own app inside Zapier, Make or n8n yet, so each recipe uses the plain webhook and HTTP steps those tools already have.

Couplr has no published app in Zapier, Make or n8n. Each of those tools has generic webhook and HTTP steps, and those are what these recipes use. Field and button names in third-party tools change over time, so the recipes name the kind of step to use and the values to enter.

Placeholders used below:

| Placeholder | Replace with |
| --- | --- |
| `YOUR_API_KEY` | A Couplr API key. See [Authentication and API keys](https://couplrfsm.com/developers/authentication). |
| `YOUR_WEBHOOK_TOKEN` | The token at the end of an inbound webhook URL. Copy the whole URL from the Integrations page. |
| `YOUR_SIGNING_SECRET` | The signing secret shown when a signed webhook or a destination was created. |
| `YOUR_UNIQUE_KEY` | A string unique to one operation, such as a UUID or the source record's id. |

## Send new leads to Couplr from any automation tool

Works with Zapier, Make, n8n and anything else that can send an HTTP `POST`. Use this when a lead arrives in another system, for example an ad lead form or a spreadsheet row, and should become a Couplr customer.

1. In Couplr, create an inbound webhook and copy its URL. See [Set up an endpoint](https://couplrfsm.com/developers/inbound-webhooks#set-up-an-endpoint).
2. In the automation tool, add its step for sending a webhook or HTTP request after your trigger.
3. Configure the step:

| Setting | Value |
| --- | --- |
| Method | `POST` |
| URL | `https://couplrfsm.com/hooks/YOUR_WEBHOOK_TOKEN` |
| Body type | JSON |
| Body | The lead's fields, for example `name`, `email`, `phone`. Any field names work. You map them in Couplr. |
| Header `X-Event-Id` | The lead's unique id in the source system, if the tool lets you add headers. |

4. Run the step once so Couplr receives a sample, then finish the mapping in Couplr and turn the webhook on.

The step-type names to look for: "Webhooks by Zapier" with the POST action in Zapier, the "HTTP" module's request action in Make, and the "HTTP Request" node in n8n.

The equivalent request with curl:

```bash
curl -X POST "https://couplrfsm.com/hooks/YOUR_WEBHOOK_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Event-Id: YOUR_UNIQUE_KEY" \
  -d '{"name": "Dana Whitfield", "email": "dana@example.com", "phone": "555-201-8890", "message": "No cooling upstairs"}'
```

## Post a website form straight to Couplr

A form on another website can post to an inbound webhook from the visitor's browser. Two rules apply, because inbound webhook URLs do not send cross-origin (CORS) headers:

- Send the form as a form post, not JSON. A JSON request from a browser on another site is blocked before it is sent.
- The page cannot read Couplr's response. Show your thank-you message once the request has been sent.

```html
<form id="contact">
  <input name="name" required>
  <input name="email" type="email">
  <input name="phone">
  <button>Send</button>
</form>
<p id="contact-status" role="status"></p>
<script>
  document.getElementById('contact').addEventListener('submit', async (event) => {
    event.preventDefault();
    const status = document.getElementById('contact-status');
    try {
      await fetch('https://couplrfsm.com/hooks/YOUR_WEBHOOK_TOKEN', {
        method: 'POST',
        mode: 'no-cors',
        body: new URLSearchParams(new FormData(event.target)),
      });
      status.textContent = 'Thanks, we will be in touch.';
    } catch (error) {
      status.textContent = 'Something went wrong. Please call us.';
    }
  });
</script>
```

> **Flip:** Careful with this one. A URL inside a web page isn't a secret any more.

The webhook URL is visible to anyone who views the page, so anyone could post to it. Use a secret-URL webhook for this, expect some spam, and do not turn on the Speed to Lead text option for a public form. To keep the URL private, or to confirm that Couplr accepted the submission, post the form to your own server and forward it from there.

## Send a signed request from your own server

Use a webhook with "Require a signature" turned on. See [Sender authentication](https://couplrfsm.com/developers/inbound-webhooks#sender-authentication).

```javascript
const crypto = require('crypto');

async function sendLead(lead) {
  const body = JSON.stringify(lead);
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const signature = crypto
    .createHmac('sha256', process.env.COUPLR_SIGNING_SECRET)
    .update(`${timestamp}.${body}`)
    .digest('hex');

  const response = await fetch(process.env.COUPLR_WEBHOOK_URL, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Hvacpro-Timestamp': timestamp,
      'X-Hvacpro-Signature': signature,
      'X-Event-Id': String(lead.id),
    },
    body,
  });

  if (!response.ok) throw new Error(`Couplr answered ${response.status}`);
}
```

```python
import hashlib, hmac, json, os, time
import requests

def send_lead(lead):
    body = json.dumps(lead)
    timestamp = str(int(time.time()))
    signature = hmac.new(
        os.environ["COUPLR_SIGNING_SECRET"].encode(),
        f"{timestamp}.{body}".encode(),
        hashlib.sha256,
    ).hexdigest()
    response = requests.post(
        os.environ["COUPLR_WEBHOOK_URL"],
        data=body,
        headers={
            "Content-Type": "application/json",
            "X-Hvacpro-Timestamp": timestamp,
            "X-Hvacpro-Signature": signature,
            "X-Event-Id": str(lead["id"]),
        },
        timeout=10,
    )
    response.raise_for_status()
```

Both send the same string they signed. Keep the secret and the URL in environment variables, not in source code.

## Look up or create a customer from an automation tool

Use the tool's HTTP request step with an API key when you need an answer back, for example the customer's Couplr id.

Find a customer by email:

| Setting | Value |
| --- | --- |
| Method | `GET` |
| URL | `https://couplrfsm.com/api/v1/customers?email=dana@example.com` |
| Header `Authorization` | `Bearer YOUR_API_KEY` |

Create one when none is found:

| Setting | Value |
| --- | --- |
| Method | `POST` |
| URL | `https://couplrfsm.com/api/v1/customers` |
| Header `Authorization` | `Bearer YOUR_API_KEY` |
| Header `Content-Type` | `application/json` |
| Header `Idempotency-Key` | `YOUR_UNIQUE_KEY` |
| Body | `{"name": "Dana Whitfield", "email": "dana@example.com", "phone": "555-201-8890"}` |

To test a connection, call `GET https://couplrfsm.com/api/v1/me`. It succeeds with any valid key.

## Book a job for a customer

Needs a key with `jobs:write`. Use the customer's id from the previous recipe and a job type name from `GET /api/v1/job-types`.

```bash
curl -X POST "https://couplrfsm.com/api/v1/jobs" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: YOUR_UNIQUE_KEY" \
  -d '{"customer_id": 1842, "job_type": "Repair", "scheduled_date": "2026-12-02", "scheduled_start_time": "09:00", "notes": "No cooling upstairs. Gate code 4410."}'
```

> **Flip:** A job you create this way is a real job. It lands on the schedule and I may assign a technician to it.

Creating a job has the same effects as creating it in the app: it appears on the schedule and may be assigned to a technician automatically.

## Copy paid invoices into another system

1. In Couplr, add an outbound webhook destination for the `invoice.paid` event, pointing at a "catch a webhook" trigger URL in your automation tool or at your own server. See [Outbound webhooks](https://couplrfsm.com/developers/outbound-webhooks).
2. Each delivery's `data.id` is the invoice id. Fetch the full invoice with its line items:

```bash
curl "https://couplrfsm.com/api/v1/invoices/9d3f6a52-0c1e-4c57-9d0a-3a0f8a4a7b21" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

3. Deliveries can repeat. Skip a delivery whose `id` you have already handled.

## Export every customer

Page through the list until the last page:

```python
import os
import requests

def all_customers():
    page = 1
    while True:
        response = requests.get(
            "https://couplrfsm.com/api/v1/customers",
            params={"page": page, "per_page": 100},
            headers={"Authorization": f"Bearer {os.environ['COUPLR_API_KEY']}"},
            timeout=30,
        )
        response.raise_for_status()
        body = response.json()
        yield from body["data"]
        if page >= body["meta"]["last_page"]:
            return
        page += 1
```

At 100 records a page and 120 requests a minute, this reads up to 12,000 customers a minute. On `429`, wait for the number of seconds in `Retry-After`.
