Gareth Wilson Gareth Wilson

Guide to Outhire Webhooks: Features and Best Practices

Published


Outhire is an AI recruiting platform: candidates apply through careers pages and forms, and recruiters and Outhire's AI agent, Joel, move them through a hiring pipeline. Its outbound webhooks push that candidate activity into the rest of your stack, so a new application can create a record in your ATS and a stage change can update your HRIS or start onboarding.

What are Outhire webhooks?

Outhire webhooks are company-level endpoints that admins manage in Settings > Webhooks. Each endpoint subscribes to one or both candidate events, has its own whsec_ signing secret, and can send up to ten custom headers of your choice. Deliveries are signed to the Standard Webhooks specification.

Every webhook also has a delivery log showing the status code, request and response headers and bodies, delivery status (sending, success or failed), timestamp and attempt count.

Outhire webhook features

FeatureDetails
ConfigurationSettings > Webhooks, admins only, one webhook per URL
Eventscandidate.applied, candidate.stage_changed, plus a test ping
Custom headersUp to 10 per webhook
SignatureStandard Webhooks HMAC-SHA256
Signature headerwebhook-signature
SecretPer webhook, whsec_..., viewable later
BodyJSON
Event routingBody field event
Response window2xx within 10 seconds
RetriesNot documented
Source IPsNot published

Events and payloads

EventTriggered whenCommon use cases
candidate.appliedA candidate submits a self-serve application (careers page or application form; not candidates created through the API or other intake)Create an ATS record, notify the hiring team
candidate.stage_changedA candidate moves between pipeline stages, from the UI, Joel (Outhire's AI agent) or the public API. Not sent if the stage is unchangedSync stage to your ATS or HRIS, trigger onboarding steps
webhook.pingAn admin clicks Send test ping. Not subscribableCheck connectivity and signature verification

The trigger rules matter. candidate.applied covers self-serve applications only, so candidates you create through the API or other intake routes don't produce one. candidate.stage_changed fires whoever moves the candidate, a recruiter, Joel or an API call, but not when the stage doesn't actually change.

This is the webhook.ping sample from the webhook samples library, the only complete example in Outhire's docs:

{
  "event": "webhook.ping",
  "occurred_at": "2026-03-11T00:00:00.000Z",
  "webhook": {
    "id": 123,
    "url": "https://example.com/webhooks/outhire"
  }
}

The candidate events' examples include placeholders, such as a resume_url of https://..., so log a real delivery before relying on their fields. There is no event ID in the body.

Setting up Outhire webhooks

  1. In Outhire, open Settings > Webhooks (/settings/webhooks). You need an admin account.
  2. Click Add webhook and enter the URL. A URL can be registered once per company, so edit the existing webhook to change its events.
  3. Select candidate.applied, candidate.stage_changed or both.
  4. Optionally add custom headers, for example an extra token or routing metadata. Names can be up to 100 characters and values up to 2000, with no case-insensitive duplicates; content-type, webhook-id, webhook-timestamp and webhook-signature are reserved.
  5. Save. Outhire generates the signing secret; copy it into OUTHIRE_WEBHOOK_SECRET, including the whsec_ prefix. You can view it again from the webhook's settings.

To route Outhire through Hookdeck, create a source with the Outhire type, add the secret, and register the source URL. For local development, the Hookdeck CLI forwards it to your machine:

npx hookdeck-cli listen 3000 outhire --path /webhooks/outhire

Then click Send test ping to send a signed webhook.ping and check your verification.

Securing Outhire webhooks

Outhire's docs give the base string {webhook-id}.{webhook-timestamp}.{raw_request_body}, signed with HMAC-SHA256 using the secret with whsec_ stripped and the rest base64-decoded. webhook-signature can contain several space-separated entries, and the function below accepts any v1, match:

const crypto = require('node:crypto');

function decodeBase64(value) {
  if (typeof value !== 'string' || !/^[A-Za-z0-9+/]+={0,2}$/.test(value)) return null;
  const bytes = Buffer.from(value, 'base64');
  return bytes.length > 0 && bytes.toString('base64').replace(/=+$/, '') === value.replace(/=+$/, '')
    ? bytes : null;
}

function verifySignature(rawBody, headers, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
  const prefix = headers['webhook-id'] !== undefined ? 'webhook' : 'svix';
  const id = headers[`${prefix}-id`];
  const timestamp = headers[`${prefix}-timestamp`];
  const signature = headers[`${prefix}-signature`];
  if (typeof id !== 'string' || !id || typeof timestamp !== 'string' ||
      !/^[0-9]+$/.test(timestamp) || typeof signature !== 'string') return false;
  const key = decodeBase64(secret.replace(/^whsec_/, ''));
  if (!key) return false;
  const message = Buffer.concat([Buffer.from(`${id}.${timestamp}.`, 'utf8'), rawBody]);
  const candidates = signature.split(/\s+/).filter((item) => item.startsWith('v1,')).map((item) => item.slice(3));
  const expected = crypto.createHmac('sha256', key).update(message).digest();
  return candidates.some((candidate) => {
    const received = decodeBase64(candidate);
    if (!received) return false;
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

Outhire also tells you to check that webhook-timestamp is within five minutes of now. That works for a direct receiver with an accurate clock. Hookdeck applies no tolerance and keeps the original timestamp when it retries or replays, so behind Hookdeck leave the check out, as above.

The receiver stores the event and answers inside Outhire's ten seconds:

const express = require('express');

// Returns the parsed body when it is a JSON object, otherwise null.
function parseJsonObject(rawBody) {
  try {
    const value = JSON.parse(rawBody.toString('utf8'));
    return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
  } catch {
    return null;
  }
}

// candidateEvents.store(deliveryId, event) saves the event and resolves once it is durable.
function createOuthireReceiver(candidateEvents) {
  const app = express();
  app.post('/webhooks/outhire', express.raw({ type: '*/*' }), async (req, res) => {
    const secret = process.env.OUTHIRE_WEBHOOK_SECRET;
    if (!secret) return res.sendStatus(500);
    if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);

    const event = parseJsonObject(req.body);
    if (!event) return res.sendStatus(400); // signed, but not a JSON object
    if (event.event === 'webhook.ping') return res.sendStatus(200);
    try {
      await candidateEvents.store(req.headers['webhook-id'], event);
    } catch {
      return res.sendStatus(503);
    }
    return res.sendStatus(200);
  });
  return app;
}

When Outhire delivers to a Hookdeck source, a bad signature still gets 200 back. Hookdeck records it as failed verification and doesn't forward it.

Make Outhire webhooks production-ready. Hookdeck Event Gateway verifies each candidate event and can send applications to your ATS and stage changes to your HRIS from one Outhire webhook.

Outhire webhook limitations and pain points

Retry behaviour isn't documented

The Problem: You don't know whether a candidate event missed during an outage will be sent again.

Why It Happens: Delivery logs show an attempt count, but Outhire doesn't publish a retry schedule, a give-up point or rules for disabling a webhook. It does say an event already delivered successfully isn't resent.

Workarounds:

  • Check the delivery log for failed deliveries after an outage, and reconcile candidates through the API.

How Hookdeck Can Help: Hookdeck accepts each event on Outhire's first attempt and retries your service on a schedule you can see.

API-created candidates don't trigger candidate.applied

The Problem: Candidates imported or created by your own integration never appear as applications.

Why It Happens: candidate.applied only fires for self-serve applications.

Workarounds:

  • Record candidates you create through the API at the time you create them, rather than waiting for a webhook.

How Hookdeck Can Help: Hookdeck's log shows exactly which events arrived, which helps confirm what Outhire does and doesn't send.

One webhook per URL

The Problem: You can't register the same URL twice with different event sets.

Why It Happens: Outhire allows each URL once per company.

Workarounds:

  • Use one webhook with both events and route on event in your code, or use different URLs.

How Hookdeck Can Help: A single Hookdeck source can split events to different destinations with filters on event.

Candidate payloads are only partly published

The Problem: You can't write a full parser for the candidate events from the docs.

Why It Happens: Their examples include placeholders, and only webhook.ping is complete.

Workarounds:

  • Capture real deliveries from a test pipeline before building on nested fields.

How Hookdeck Can Help: Hookdeck stores every payload, so you can inspect real candidate events while you build.

Best practices

Deduplicate on webhook-id

There's no event ID in the body, and Outhire asks receivers to be idempotent. Store the header value; the idempotency guide covers the patterns.

Keep candidate data out of logs

Candidate events include personal details and résumé links. Log IDs and event names only.

Treat custom headers as extras

Custom headers can carry an extra token or routing metadata such as an environment tag. Keep verifying the signature even when you add a token header.

Keep your clock in sync

If you verify directly with Outhire's five-minute check, run NTP on the receiver, or timestamps will fail.

Conclusion

Outhire sends two candidate events with Standard Webhooks signing, a signed test ping and a detailed delivery log. The retry policy and parts of the candidate payload aren't documented, so capture a real delivery before building on it.

Hookdeck Event Gateway brings retries, routing and payload history to Outhire's candidate events. Try Hookdeck at no cost.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.