Guide to Workable Webhooks: Features and Best Practices
Workable webhooks tell your systems when something changes in your hiring pipeline or HR records: a candidate applies or moves to a new stage, an employee record is created, updated or published, onboarding is completed, or a time-off request is approved. They are how Workable data reaches your ATS integrations, HRIS syncs and provisioning scripts without polling the API.
This guide covers how Workable webhook subscriptions are created, which key signs them and what the docs leave out, the candidate and employee events and their payloads, and the best practices for running a Workable receiver in production.
What are Workable webhooks?
Workable calls its webhooks subscriptions. You register one through the Workable SPI (its API) by posting a target URL and the event you want. When that event happens, Workable sends a POST to the URL with the associated resource: a candidate object for candidate events, or employee, onboarding or time-off data for HR events.
Every request is signed with an X-Workable-Signature header holding an HMAC-SHA256 of the request body. Workable has no separate webhook secret. The key is the account API token that you used to create the subscription, or the OAuth application secret if your integration uses OAuth.
Workable webhook features
| Feature | Details |
|---|---|
| Configuration | The API: POST https://{subdomain}.workable.com/spi/v3/subscriptions |
| Required scope | r_candidates or r_employees |
| Subscription model | One event per subscription |
| Filters | job_shortcode and stage_slug in args, for candidate events only |
| Target URLs | Must be unique. A duplicate returns 409 Conflict |
| Event name | event_type in the body |
| Signature header | X-Workable-Signature |
| Scheme | HMAC-SHA256 of the request body. Encoding not documented (hex in practice) |
| Key | The account API token, or the OAuth application secret |
| Replay window | None. No timestamp is signed |
| Success | 200. Anything else is treated as an error and the request is replayed |
Workable doesn't document a response timeout, the retry schedule or source IP addresses for webhooks.
Common events
Workable lists nine events:
| Event | Resource | Notes |
|---|---|---|
candidate_created | Candidate | A candidate is added |
candidate_moved | Candidate | A candidate moves to another pipeline stage |
candidate_deleted | Candidate | A candidate is deleted |
job_deleted | Job | A job is deleted |
employee_created | Employee | Minimal employee information |
employee_updated | Employee | The fields that were updated |
employee_published | Employee | Only the employee ID |
onboarding_completed | Employee | The employee finishes the onboarding workflow, with onboarding content |
timeoff_updated | Employee | A time-off request changes |
Two details don't line up in Workable's docs. The /subscriptions endpoint's description of event lists seven values and leaves out candidate_deleted and job_deleted. And you subscribe to timeoff_updated, but Workable's time-off examples arrive with an event_type of timeoff_request_approved or timeoff_request_reverted. Route time-off events on those values.
Every body has the same envelope. Here is Workable's employee_created example, trimmed:
{
"id": "4d3",
"resource_type": "employee",
"event_type": "employee_created",
"fired_at": "2024-01-08T14:01:06.217Z",
"data": {
"id": "4d3",
"status": "status_draft",
"firstname": "Obediah",
"lastname": "Kydde",
"job_title": "Software Engineer",
"start_date": "2022-09-13T16:56:33.000Z",
"employee_number": "1"
}
}
id is the event identifier, resource_type is candidate or employee, and data is the resource. For candidate events, data is a full candidate object similar to the candidate details endpoint, including the job (shortcode, title), stage, education, experience and contact details.
The candidate payload reflects the resource at the time Workable sends the request, not when the event fired. Workable says these are usually the same, but race conditions can make them differ. For employees, Workable's own flow is to wait for employee_published and then fetch the complete record with GET /employees/:id.
Setting up Workable webhooks
Subscriptions are created through the API with an access token that has the r_candidates or r_employees scope.
curl -X POST "https://$WORKABLE_SUBDOMAIN.workable.com/spi/v3/subscriptions" \
-H "Authorization: Bearer $WORKABLE_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"target": "https://example.com/webhooks/workable",
"event": "candidate_moved",
"args": {
"account_id": "your-subdomain",
"job_shortcode": "",
"stage_slug": "hired"
}
}'
targetandeventare required. The response is the subscriptionid.- For candidate events,
argsfilters by job and stage. If you includeargs,account_id,job_shortcodeandstage_slugare all required. Use an empty string for all jobs or all stages. - Leave
argsout for employee events, which don't use it. - List subscriptions with
GET /subscriptionsand remove one withDELETE /subscriptions/:id.
Because target URLs must be unique, you can't point two subscriptions at exactly the same URL. Give each one its own path or query string, for example /webhooks/workable?event=candidate_.
Store the key that will sign the requests: the API token you used to create the subscription, or your OAuth application's secret. If you use Hookdeck, create a Workable source and paste that same value into its signing key field. Each subscription needs a unique target, so add a distinct path to the Hookdeck source URL for each event.
Securing Workable webhooks
Compute an HMAC-SHA256 of the raw request body with the API token or OAuth app secret, and compare it with X-Workable-Signature in constant time.
Workable's docs name the header and the algorithm but not the encoding. Lowercase hex is what the maintained n8n and Botpress Workable integrations use, and it's what Hookdeck's Workable source expects. Capture one real delivery and confirm the format before you rely on it.
const crypto = require('crypto');
const express = require('express');
function verifyWorkableSignature(rawBody, signatureHeader, key) {
if (!signatureHeader) return false;
const expected = crypto.createHmac('sha256', key).update(rawBody).digest('hex');
const received = Buffer.from(signatureHeader.trim().toLowerCase(), 'utf8');
const computed = Buffer.from(expected, 'utf8');
return received.length === computed.length && crypto.timingSafeEqual(received, computed);
}
const app = express();
app.post('/webhooks/workable', express.raw({ type: '*/*' }), (req, res) => {
// The account API token that created the subscription, or the OAuth app secret.
const key = process.env.WORKABLE_SIGNING_KEY;
if (!key) return res.sendStatus(500); // fail closed
if (!verifyWorkableSignature(req.body, req.get('x-workable-signature'), key)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString('utf8'));
// Route on event.event_type, deduplicate on event.id, then process asynchronously.
res.sendStatus(200);
});
The same check in Python:
import hashlib
import hmac
def verify_workable_signature(raw_body: bytes, signature_header, key: str) -> bool:
if not signature_header:
return False
expected = hmac.new(key.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
received = signature_header.strip().lower().encode("utf-8")
return hmac.compare_digest(received, expected.encode("utf-8"))
# In FastAPI: raw_body = await request.body()
# verify_workable_signature(raw_body, request.headers.get("x-workable-signature"),
# os.environ["WORKABLE_SIGNING_KEY"])
Make Workable webhooks production-ready. Hookdeck Event Gateway verifies the
X-Workable-Signatureheader, so your services don't each need a copy of your Workable API token, and durably queues every candidate and employee event before your service sees it.
Workable webhook limitations and pain points
The signing key is a full API credential
The Problem: To verify webhooks, every service that receives them needs your Workable API token or OAuth app secret.
Why It Happens: Workable has no dedicated webhook secret. It signs with the credential that created the subscription, which can also read and change data through the API.
Workarounds:
- Verify in one place and pass verified events on, so only that component holds the token.
- Create subscriptions with a token scoped to
r_candidatesorr_employeesonly, if your setup allows. - If you replace the token, expect the signing key to change with it, and update the verifier at the same time.
How Hookdeck Can Help: Hookdeck verifies the signature with the key stored on the source, so your services receive already-verified events and never hold the Workable credential.
The encoding isn't documented
The Problem: Your verifier rejects every delivery, and Workable's docs don't tell you what format to expect.
Why It Happens: Workable says only "HMAC SHA256 on the payload". It doesn't say whether the digest is hex or base64, or give a code sample. Third-party integrations agree on lowercase hex.
Workarounds:
- Start with lowercase hex, as above.
- If nothing matches, log the header from one delivery. A 64-character value made only of hex digits is hex, and a 44-character value ending in
=is base64. - Check you're using the right key: the token that created that subscription, or the OAuth app secret for OAuth integrations.
How Hookdeck Can Help: Hookdeck shows each request's headers and its verification result, so you can see what Workable sent and whether it verified before writing any code.
Payloads can describe a later state
The Problem: A candidate_moved event shows a stage the candidate has already left, or details that changed after the event.
Why It Happens: Workable builds the candidate payload when it sends the request, not when the event fired. A retry sent later carries even newer data.
Workarounds:
- Use
fired_atfor ordering, and treatdataas the latest known state rather than a historical snapshot. - For decisions that matter, fetch the candidate or employee from the API.
How Hookdeck Can Help: Hookdeck stores every event exactly as it arrived, so you can see what Workable sent and when, even if the resource has changed since.
Retries without a schedule
The Problem: You don't know how long Workable keeps retrying, or when it stops.
Why It Happens: Workable says only that a non-200 response is an error and the request is replayed. There's no documented timeout, attempt count or retry window.
Workarounds:
- Return
200exactly, as soon as the event is verified and stored. - Deduplicate on the event
id, since a retry can follow a timeout your server didn't notice.
How Hookdeck Can Help: Hookdeck acknowledges Workable as soon as each request is stored, then retries delivery to your service on a schedule you control and lets you replay anything that failed.
Best practices
Verify against the raw body
The signature covers the exact bytes Workable sent. In Express, mount express.raw() on the webhook route and parse after verification. In a Next.js route handler, read await request.text(). In FastAPI, read await request.body().
Filter candidate events at the source
Use job_shortcode and stage_slug so you only receive what you act on. A subscription to candidate_moved with stage_slug set to hired, for example, is a clean trigger for onboarding.
Route on event_type, not the subscription
Route on the body's event_type, and handle the time-off values timeoff_request_approved and timeoff_request_reverted as well as any you don't recognise yet.
Deduplicate on id
The top-level id is the event identifier. Store it before processing and skip repeats. See our guide to webhook idempotency.
Acknowledge fast and process asynchronously
Return 200 as soon as the request is verified and stored. See why to process webhooks asynchronously.
Conclusion
Workable signs each webhook with an HMAC-SHA256 of the raw body in X-Workable-Signature, keyed with the account API token or OAuth app secret rather than a webhook secret. Use lowercase hex and confirm it against a real delivery, give each subscription a unique URL, route on event_type (time-off events arrive under their own names), treat data as the latest state, and answer with 200 quickly.
Hookdeck Event Gateway sits in front of your receiver, checks every Workable signature, and holds each event until your service has processed it.
Get started with Hookdeck for free and handle Workable webhooks reliably in minutes.