> ## Documentation Index
> Fetch the complete documentation index at: https://docs.canary.effectiveai.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Receive webhooks

> Get a signed POST when an answer finishes or an employer gets new OSHA activity.

Instead of checking back, register a URL and Effective sends it a small signed event when something
happens. The event says what changed and links to the resource; read the resource for the details.

## Create an endpoint

```bash theme={null}
curl --request POST \
  --url "https://api.canary.effectiveai.app/api/v2/webhook-endpoints" \
  --header "Authorization: Bearer $EFFECTIVE_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "url": "https://example.com/effective-webhooks",
    "enabledEvents": ["osha.employer.event_added", "answer.completed", "answer.failed"],
    "filters": { "states": ["FL"] }
  }'
```

The response includes `secret`. It is shown only once, so store it now.

```json theme={null}
{
  "id": "whk_0123456789abcdef0123",
  "url": "https://example.com/effective-webhooks",
  "enabledEvents": ["answer.completed", "answer.failed", "osha.employer.event_added"],
  "filters": { "states": ["FL"] },
  "status": "enabled",
  "secret": "whsec_…"
}
```

Filters apply only to events that carry that field: `employerIds` and `states` narrow OSHA events
and are ignored for answers. Only events that happen after you create the endpoint are sent.

## What you receive

```json theme={null}
{
  "id": "evt_9f2c41d07a3b5e8c6d10",
  "type": "osha.employer.event_added",
  "occurredAt": "2026-10-22T12:41:07.000Z",
  "subject": {
    "type": "employer",
    "id": "emp_0123456789abcdef0123",
    "$uri": "/api/v2/insurance/employers/emp_0123456789abcdef0123"
  },
  "data": {
    "eventType": "inspection",
    "eventDate": "2026-09-30",
    "siteId": "site_0123456789abcdef0123",
    "state": "FL",
    "feins": [],
    "path": "/insurance/osha/inspections/1234567"
  }
}
```

For OSHA events, `data.path` is the OSHA row behind the event, relative to `/api/v2`, as in the
[employer guide](/guides/osha-employers).

| Event | Sent when |
| - | - |
| `answer.completed` | An answer finished. Read `subject.$uri` for the result. |
| `answer.failed` | An answer failed. `data.failureCode` says why. |
| `osha.employer.event_added` | New OSHA activity appeared for an employer. Needs the OSHA dataset |
| `webhook_endpoint.test` | You called `testWebhookEndpoint` |

## Verify and respond

Each request has an `Effective-Signature` header like `t=1761136867,v1=5257a8…`. Compute an
HMAC-SHA256 of `<t>.<raw body>` with your secret and compare it to `v1`. Reject requests whose `t`
is more than five minutes old.

```js theme={null}
import { createHmac, timingSafeEqual } from 'node:crypto';

function verify(rawBody, header, secret) {
  const { t, v1 } = Object.fromEntries(header.split(',').map((part) => part.split('=')));
  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
  return (
    fresh &&
    v1?.length === expected.length &&
    timingSafeEqual(Buffer.from(v1), Buffer.from(expected))
  );
}
```

Return any 2xx status within 10 seconds. Anything else is retried with backoff for three days; after
that the endpoint is disabled and shows `"disabledReason": "failing"`. An event can arrive more than
once or out of order, so skip IDs you have already handled.

To check your receiver, send a test event with
`POST /api/v2/webhook-endpoints/{id}/test`. Webhooks have no usage charge.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.