# Outhire Webhooks

[Outhire](https://outhire.ai) is an AI recruiting platform. Its outbound webhooks push candidate lifecycle events (a candidate applies, a candidate changes pipeline stage) to your systems. Each delivery is a JSON `POST` signed with **Standard Webhooks**: `webhook-id`, `webhook-timestamp` and `webhook-signature` headers and a per-webhook `whsec_` secret.

## When to Use This Skill

* How do I receive Outhire webhooks?
* How do I verify an Outhire webhook signature?
* Where do I find my Outhire webhook signing secret?
* How do I handle `candidate.applied` or `candidate.stage_changed` events?
* Why is my Outhire `webhook-signature` verification failing?
* How do I dedupe Outhire webhooks when the payload has no event ID?

## Verification (core)

| Header | Value |
| --- | --- |
| `webhook-id` | Unique ID of this delivery |
| `webhook-timestamp` | Unix seconds when the request was sent |
| `webhook-signature` | One or more space-separated `v1,<base64 HMAC-SHA256>` entries |

Signed content is `{webhook-id}.{webhook-timestamp}.{raw body}`. The key is the base64-**decoded** part of the secret after `whsec_`; keep the full `whsec_` value in `OUTHIRE_WEBHOOK_SECRET`. The examples use `standardwebhooks` (npm, PyPI), which compares in constant time against every `v1,` entry. Hookdeck also accepts the `svix-*` spelling of the same headers, so the examples read `webhook-*` and fall back to `svix-id`, `svix-timestamp` and `svix-signature` when `webhook-id` is absent.

```javascript
const { Webhook } = require('standardwebhooks');

// rawBody: exact request body string, NOT re-serialised JSON
function verifyOuthireWebhook(rawBody, get, secret = process.env.OUTHIRE_WEBHOOK_SECRET) {
  const prefix = get('webhook-id') ? 'webhook' : 'svix'; // svix-* fallback
  return new Webhook(secret).verify(rawBody, {
    'webhook-id': get(`${prefix}-id`),
    'webhook-timestamp': get(`${prefix}-timestamp`),
    'webhook-signature': get(`${prefix}-signature`),
  }); // returns the parsed event; throws → respond 400
}

```

Python: `from standardwebhooks.webhooks import Webhook`, then `Webhook(secret).verify(await request.body(), {...same mapping...})`.

> **Retries and the 5-minute window.** The library always enforces a fixed 5-minute timestamp tolerance. Hookdeck retries and replays resend the **original** timestamp, so the library rejects a late retry. If you receive Outhire webhooks through Hookdeck, use the manual verifier in [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/outhire-webhooks/references/verification.md) (HMAC-SHA256 over `{id}.{timestamp}.{raw body}`, every `v1,` entry compared in constant time) where the window can be dropped or widened.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/outhire-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/outhire-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/outhire-webhooks/examples/fastapi/).

## Event Envelope

```json
{ "event": "candidate.stage_changed", "occurred_at": "2026-03-11T00:00:00.000Z",
  "candidate": { "id": 123, "name": "Jane Doe" }, "job": { "id": 456, "title": "Account Executive" },
  "company": { "id": 789 },
  "stage_change": { "previous_stage": "applied", "new_stage": "interview", "changed_by": "user" } }

```

The top-level field is `event`. There is **no event ID** in the body: dedupe on the `webhook-id` header.

## Common Event Types

| Event | Fired when |
| --- | --- |
| `candidate.applied` | A candidate submits a self-serve application (not candidates created via API or other intake) |
| `candidate.stage_changed` | A candidate moves between pipeline stages (`changed_by`: `user`, `joel` or `api`) |
| `webhook.ping` | An admin clicks **Send test ping**. A signed delivery, not a challenge |

These three are the documented events; the list may not be exhaustive, so acknowledge unknown events with `2xx`.

## Delivery Semantics

* Return a `2xx` within **10 seconds**. Enqueue the event and process it asynchronously.
* Outhire keys each delivery and doesn't resend one that already succeeded, but be idempotent anyway.
* The docs don't describe retries or ordering. Use `occurred_at` if you need to order events.
* Missing headers or a bad signature: respond `400`. Handlers fail closed (`500`) when `OUTHIRE_WEBHOOK_SECRET` is unset.

## Environment Variables

```bash
OUTHIRE_WEBHOOK_SECRET=whsec_xxxxx   # full value including the prefix; one per webhook

```

Get it from **Settings > Webhooks** after saving the webhook (admins only).

## Local Development

```bash
# Express / Next.js (use 8000 for FastAPI). No install or account required.
npx hookdeck-cli listen 3000 outhire --path /webhooks/outhire

```

Add the printed URL with **Add webhook** in **Settings > Webhooks**, subscribe to the events, then use **Send test ping**.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/outhire-webhooks/references/overview.md) - Events, payload fields, delivery behavior
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/outhire-webhooks/references/setup.md) - Settings > Webhooks setup, custom headers, signing secret
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/outhire-webhooks/references/verification.md) - Library and manual verification, gotchas, debugging