# Guide to Coterie Webhooks: Features and Best Practices

Coterie webhooks keep a partner's systems in step with Coterie Insurance: an application becomes ready to quote, a quote is priced, or a policy is issued. If you sell or embed Coterie's small-business commercial insurance through its Commercial Insurance API, webhooks are how those updates reach your platform in real time instead of through polling.

This guide covers how Coterie webhooks are enabled and authenticated, how to tell the three kinds of webhook apart, what Coterie leaves undocumented, and the practices that keep a Coterie receiver reliable.

## What are Coterie webhooks?

Coterie uses webhooks "for three different actions; applications, quotes, and policies". Each webhook body is the resource itself, in the same shape the API uses: an application object, a quote object or a policy object. There is no envelope, no event name and no type field.

Webhooks are not self-serve. Coterie's documentation describes no dashboard or API for registering an endpoint, and its optional headers "must be turned on by request only", so you set webhooks up with your Coterie contact.

Coterie does not sign its payloads. The only authentication is an optional static header named `Authentication`, sent as `Authentication: token {guid}`, which your receiver compares with the token it expects.

## Coterie webhook features

| Feature | Details |
| --- | --- |
| Configuration | Through your Coterie contact |
| Kinds | Applications, quotes and policies |
| Event identity | None on the wire. Classify by the ID key in the body |
| Body | The bare API resource, JSON |
| Authentication | Optional `Authentication: token {guid}` header, enabled on request |
| Signature | None |
| Optional headers | `x-timeStamp`, `x-applicationId`, `x-leadId`, each enabled on request |
| Timestamp | `x-timeStamp`, epoch milliseconds, not signed |
| Method, timeout, retries | Not documented |
| Source IPs | Not published |

## Common events

Because no field names the kind of webhook, classify each body by the identifier it carries, in this order:

| Kind | Recognise it by | Notable fields |
| --- | --- | --- |
| Policy | `PolicyId` | `PolicyNumber`, `Status` (for example `Pending`), `StartDate`, `EndDate`, `Premium` |
| Quote | `quoteId` | `PolicyType`, `Premium`, `MonthlyPremium`, `IsEstimate`, `LineItems`, `applicationId` |
| Application | `applicationId`, with neither of the above | `Status` (for example `ReadyToQuote`), `BusinessName`, `ApplicationTypes`, `Locations` |

The order matters. A quote body also carries `applicationId`, so checking for `applicationId` first files every quote as an application.

Here is a trimmed version of Coterie's sample quote webhook:

```json
{
  "quoteId": "323d6e60-cb75-4098-af71-a643d8f47dba",
  "PolicyType": "BOP",
  "ExpirationDate": null,
  "IsEstimate": true,
  "Premium": 2699,
  "MonthlyPremium": null,
  "ApplicationUrl": "https://quote-test.coterieinsurance.com/application/83857350-4bfb-4669-862c-61f05a7a4be9",
  "applicationId": "83857350-4bfb-4669-862c-61f05a7a4be9",
  "LineItems": [
    {
      "Description": "BPP Premium",
      "Amount": 74.97,
      "LineItemType": "Premium",
      "PremiumType": "BPP"
    }
  ]
}

```

Keys are mixed-case: `applicationId` and `quoteId` start in lower case, but `PolicyId`, `Status` and most other fields don't. Match them exactly.

Application and policy bodies carry a `Metadata` field. It is a JSON-encoded string, not an object, holding whatever you attached to the application, for example `{"partner_id": "...", "lead_id": "...", "customer_id": "CUSTID123"}`. Parse it a second time to get your own IDs back. If you ask for the `x-leadId` header, Coterie sends your lead ID from that metadata as a header too.

Coterie doesn't document which changes trigger each kind of webhook, so a policy webhook can't tell you by itself whether the policy was just created or changed. Compare `Status` with what you stored last time.

## Setting up Coterie webhooks

1. Give your Coterie contact your HTTPS endpoint URL and the kinds of webhook you need.
2. Ask them to enable the `Authentication` header, and confirm the exact token value with them. Coterie's documentation doesn't say which side generates the GUID.
3. Store the full header value, including the `token ` prefix, for example as `COTERIE_AUTHENTICATION=token 1a2b3c4d-...`.
4. Optionally, ask for `x-timeStamp`, `x-applicationId` and `x-leadId`.

The sample payloads point at `quote-test.coterieinsurance.com`, so Coterie runs a separate test environment. Confirm with your contact which environment sends to which endpoint, and keep a token for each.

If you use Hookdeck, create a Coterie source and paste the **full** header value into its **Authentication Header Value** field, including the `token ` prefix, as in `token <guid>`. Hookdeck compares the whole value, so a bare GUID never matches. Use the source URL as your webhook endpoint. If the `Authentication` header hasn't been enabled yet, leave verification off on the source until it is.

Hookdeck doesn't check this header at the edge. It accepts the request and answers Coterie with `200`, then records any request with a missing or wrong token as failed verification instead of delivering it to your service.

## Securing Coterie webhooks

There is nothing to compute. Read the `Authentication` header (not `Authorization`) and compare the whole value with the token you stored, in constant time. Because the body is not signed, you can parse it normally after the check.

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

function verifyCoterieToken(headerValue, expectedValue) {
  if (!headerValue || !expectedValue) return false;
  const received = Buffer.from(headerValue, 'utf8');
  const expected = Buffer.from(expectedValue, 'utf8');
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

function classifyCoterieWebhook(body) {
  if ('PolicyId' in body) return 'policy';
  if ('quoteId' in body) return 'quote'; // check before applicationId
  if ('applicationId' in body) return 'application';
  return 'unknown';
}

const app = express();

app.post('/webhooks/coterie', express.raw({ type: '*/*' }), (req, res) => {
  const expected = process.env.COTERIE_AUTHENTICATION; // "token <guid>"
  if (!expected) return res.sendStatus(500); // fail closed

  if (!verifyCoterieToken(req.get('authentication'), expected)) {
    return res.sendStatus(401);
  }

  let body;
  try {
    body = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }
  const kind = classifyCoterieWebhook(body);
  // Persist, then process async.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hmac

def verify_coterie_token(header_value, expected_value: str) -> bool:
    if not header_value or not expected_value:
        return False
    return hmac.compare_digest(header_value.encode("utf-8"), expected_value.encode("utf-8"))

def classify_coterie_webhook(body: dict) -> str:
    if "PolicyId" in body:
        return "policy"
    if "quoteId" in body:
        return "quote"
    if "applicationId" in body:
        return "application"
    return "unknown"

# In FastAPI: verify_coterie_token(request.headers.get("authentication"),
#                                  os.environ["COTERIE_AUTHENTICATION"])

```

The token is a static credential sent with every request. It proves only that the sender knows it, it doesn't protect the body, and anyone who captures one request can send others. Serve the endpoint over HTTPS only and treat the token like a password.

> **Make Coterie webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) checks the `Authentication` token, answers Coterie straight away, and durably queues every application, quote and policy update before your service sees it.

## Coterie webhook limitations and pain points

### A static token instead of a signature

**The Problem:** Nothing proves the body came from Coterie unaltered, and without the optional header nothing authenticates the request at all.

**Why It Happens:** Coterie doesn't sign payloads. The `Authentication` header is the only credential, it's the same on every request, and it is off until you ask for it.

**Workarounds:**

* Ask for the `Authentication` header before you go live, and reject requests without it.
* Treat a webhook as a notification for anything that moves money or coverage, and confirm the policy or quote through the Coterie API before acting.
* Keep the token out of logs and rotate it with your contact if it may have leaked.

**How Hookdeck Can Help:** Hookdeck checks the token for you, so your own services never hold it, and keeps requests with a missing or wrong token away from your service.

### No type field

**The Problem:** Quotes are processed as applications, or unexpected bodies crash the handler.

**Why It Happens:** Coterie sends the bare resource, so the only clue to its kind is which ID key is present. The quote body contains `applicationId` as well as `quoteId`.

**Workarounds:**

* Classify by `PolicyId`, then `quoteId`, then `applicationId`, and send anything else to an "unknown" path that logs and acknowledges.
* Write a test for each of Coterie's sample bodies.

**How Hookdeck Can Help:** Hookdeck filters can route on the presence of `PolicyId` or `quoteId`, so each kind of webhook can go to its own destination.

### Undocumented delivery behaviour

**The Problem:** You don't know what response Coterie expects, how long it waits, or whether it retries.

**Why It Happens:** Coterie's documentation covers the payloads and headers but says nothing about delivery: no method, timeout, retry schedule or failure handling.

**Workarounds:**

* Respond with `200` quickly and process asynchronously.
* Ask your Coterie contact about retries and timeouts, and record the answer with your integration notes.
* Reconcile open applications and policies through the API on a schedule.

**How Hookdeck Can Help:** Hookdeck accepts each webhook as soon as Coterie sends it and stores it, then retries delivery to your service on a schedule you control and lets you replay anything that failed.

### No event ID

**The Problem:** You can't tell a duplicate from a genuine second update.

**Why It Happens:** Bodies carry the resource ID but no event ID, and `x-timeStamp` is optional and unsigned.

**Workarounds:**

* Make updates idempotent: store the latest `Status` and other key fields per resource, and ignore a webhook that changes nothing.
* If you enable `x-timeStamp`, use it with the resource ID to order updates, not to authenticate them.

**How Hookdeck Can Help:** Hookdeck's deduplication can drop repeated deliveries based on the body fields you choose, such as `PolicyId` and `Status`.

## Best practices

### Store the full token, prefix included

The header value is `token {guid}`. Store and compare the whole string. Comparing a bare GUID, or reading `Authorization` instead of `Authentication`, fails every request.

### Parse `Metadata` twice

`Metadata` is a string containing JSON. Parse the body, then parse `Metadata`, and handle `null`, which the sample policy body uses.

### Keep test and production apart

Use separate endpoints, or at least separate tokens, for Coterie's test and production environments, and check `ApplicationUrl` or your own metadata before acting on a webhook.

### Acknowledge fast and process asynchronously

Coterie documents no timeout, so keep the request path short: check the token, store the body, return `200`. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks) and our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

## Conclusion

Coterie sends the application, quote or policy object itself, with no type field and no signature. Ask your Coterie contact to enable the `Authentication: token {guid}` header, compare the whole value in constant time, classify each body by `PolicyId`, then `quoteId`, then `applicationId`, and confirm important changes through the API, since Coterie documents no retry behaviour.

[Hookdeck Event Gateway](https://hookdeck.com) sits in front of your receiver, checks the Coterie token, and holds each webhook until your service has processed it.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Coterie webhooks reliably in minutes.