Guide to JobAdder Webhooks: Features and Best Practices
JobAdder webhooks notify your application when recruitment data changes. A job's status changes, a candidate is updated, an application moves to a new stage, a placement is approved, an interview is scheduled: each can reach your service as an HTTP request, so a partner integration or internal tool can react without polling the JobAdder API.
This guide covers how JobAdder webhooks are created and scoped, the notification envelope and event list, how to authenticate requests with the authorization value, and what JobAdder leaves undocumented about delivery.
What are JobAdder webhooks?
JobAdder webhooks are created by an application through the Webhook API, not in the JobAdder interface. The application needs the offline_access scope, and each event needs its own read scope, such as read_candidate for candidate events. A webhook has a name, a URL, a list of events and a status.
When a subscribed event happens, JobAdder sends an HTTP POST to the URL. The body is a JSON envelope that names the event in event and carries an eventId for deduplication.
JobAdder doesn't sign notifications. A webhook has an optional authorization field, described as an "optional authorization header to include with the HTTP POST notification". JobAdder sends that string as the Authorization header, exactly as you set it. The API's example value is a Basic credential, Basic dXNlcm5hbWU6cGFzc3dvcmQ=.
JobAdder webhook features
| Feature | Details |
|---|---|
| Management | Webhook API: POST /webhooks, GET, PUT and DELETE /webhooks/{webhookId} |
| Scopes | offline_access plus each event's read scopes. Partner action events also need partner_ui_action |
| Method | POST |
| Envelope | apiVersion, event, eventId, eventDate, eventUser, eventData, then event details |
| Authentication | Optional authorization value, sent verbatim as Authorization. No signature |
| Filters | eventFilters for jobapplication_, by statusId or active status |
| Status | Enabled, Disabled, Suspended, and Failed (with failedAt) |
| Ordering and duplicates | Order not guaranteed; duplicates possible |
| Retries, timeout, source IPs | Not documented |
Common events
| Area | Events |
|---|---|
| Jobs | job_status_changed, job_invoice_sent, job_partner_action |
| Job ads | jobad_posted, jobad_expired, jobad_partner_action |
| Candidates | candidate_status_changed, candidate_updated, candidate_skill_updated, candidate_partner_action |
| Applications | jobapplication_, jobapplication_ |
| Placements | placement_approved, placement_status_changed, placement_partner_action |
| Companies | company_status_changed, company_updated, company_address_updated, company_partner_action |
| Contacts | contact_status_changed, contact_updated, contact_partner_action |
| Interviews and events | interview_scheduled, interview_, event_scheduled, event_ |
| Folders | folder_job_added, folder_candidate_added, folder_placement_added and the matching _removed events for jobs, candidates, applications, placements, companies and contacts, plus folder_updated |
| Other | user_suspended, sms_sent, opportunity_ |
JobAdder keeps adding events: its release notes list opportunity_, contact_updated, company_updated, company_address_updated and candidate_skill_updated as additions in 2025 and 2026.
The notification envelope looks like this. JobAdder's example ends with an <event details> placeholder rather than real fields:
{
"apiVersion": 2,
"event": "placement_approved",
"eventId": "00000000-0000-0000-0000-000000000000",
"eventDate": "2019-01-01T09:00:00Z",
"eventUser": {},
"eventData": {}
}
Route on event, deduplicate on eventId, and capture a real notification for each event you handle to see its details.
Inspect JobAdder webhooks as they arrive. Create a webhook pointing at a Hookdeck Inspect URL to see each event's full body and the
Authorizationheader, with no account or setup required.
Setting up JobAdder webhooks
Choose the full Authorization value first, for example Bearer followed by the output of openssl rand -hex 32. Any string works, because JobAdder sends it unchanged.
Create the webhook with an access token that has offline_access and the read scopes for your events. JobAdder's token response includes an api field with the base URL to use; the example below uses https://api.jobadder.com/v2:
curl --request POST \
--url https://api.jobadder.com/v2/webhooks \
--header "Authorization: Bearer $JOBADDER_ACCESS_TOKEN" \
--header 'Content-Type: application/json' \
--data '{
"name": "Placement sync",
"url": "https://example.com/webhooks/jobadder",
"events": ["placement_approved", "placement_status_changed"],
"authorization": "Bearer YOUR_LONG_RANDOM_VALUE",
"status": "Enabled"
}'
Store the webhookId from the response. The webhook representation JobAdder returns doesn't include the authorization value, so keep your own copy; you can't read it back later. To change it, send the new value with PUT /webhooks/{webhookId}.
To route through Hookdeck, create a source with the JobAdder type and use its URL as url. The source has one field, Authorization Header. As Hookdeck's setup note says, paste the authorization value exactly as set in the JobAdder webhook, including any Bearer or Basic prefix. Hookdeck compares the whole header with it and accepts POST.
Securing JobAdder webhooks
Compare the whole Authorization header with the value you set, in constant time. Don't strip the prefix or decode a Basic value.
const crypto = require('crypto');
const express = require('express');
// Exactly the authorization value set on the webhook, e.g. "Bearer 3f9c...".
const EXPECTED = process.env.JOBADDER_AUTHORIZATION;
function safeEqual(received, expected) {
const a = Buffer.from(received, 'utf8');
const b = Buffer.from(expected, 'utf8');
// timingSafeEqual throws on unequal lengths, so check first.
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
const app = express();
app.post('/webhooks/jobadder', express.raw({ type: '*/*' }), (req, res) => {
const received = req.get('authorization');
if (!EXPECTED || typeof received !== 'string' || !safeEqual(received, EXPECTED)) {
return res.sendStatus(401);
}
let notification;
try {
notification = JSON.parse(req.body.toString('utf8'));
} catch {
return res.sendStatus(400);
}
// Store notification.eventId, queue by notification.event, and respond.
res.sendStatus(200);
});
The same check in Python:
import hmac
import os
EXPECTED = os.environ.get("JOBADDER_AUTHORIZATION", "") # e.g. "Bearer 3f9c..."
def verify_jobadder_request(headers) -> bool:
if not EXPECTED:
return False # never accept when unconfigured
received = headers.get("authorization")
if not received:
return False
# Compare bytes: compare_digest raises TypeError on a non-ASCII str.
return hmac.compare_digest(received.encode("utf-8"), EXPECTED.encode("utf-8"))
A static Authorization value authenticates the sender, not the body. It cannot detect a modified payload, and with no signature or timestamp a captured request can be replayed and still pass. Recruitment data is personal data, so use HTTPS, a long random value rather than a reusable username and password, and rotate it with PUT /webhooks/{webhookId} if it might have leaked.
Make JobAdder webhooks production-ready. Hookdeck Event Gateway checks the
Authorizationvalue, accepts every notification as it arrives, and queues it durably for your integration.
JobAdder webhook limitations and pain points
Delivery rules aren't documented
The Problem: You don't know how long JobAdder waits, whether it retries, or what moves a webhook to Suspended or Failed.
Why It Happens: The API documentation describes the envelope, events and webhook states but not the delivery policy.
Workarounds:
- Respond quickly and process in the background, so timeouts are unlikely.
- Poll
GET /webhooks/{webhookId}and alert ifstatuschanges orfailedAtis set. - Reconcile important records against the API periodically.
How Hookdeck Can Help: Hookdeck accepts and stores each notification, so JobAdder sees successful deliveries even when your service is down. Hookdeck then retries delivery to you on your schedule, and you can replay any notification.
Duplicates and out-of-order events
The Problem: A candidate update is processed twice, or an older status change overwrites a newer one.
Why It Happens: JobAdder says the order of notifications isn't guaranteed and duplicates may occur.
Workarounds:
- Deduplicate on
eventId. - Compare
eventDatewith the last change you applied, or re-read the record from the API before writing.
How Hookdeck Can Help: Deduplication can discard repeats before they reach your service, and every notification is stored with its arrival time for debugging.
Missing scopes mean missing events
The Problem: A webhook is created but some events never arrive.
Why It Happens: Every event needs offline_access plus its own read scopes, and partner action events also need partner_ui_action.
Workarounds:
- Check the scope table for each event before you request authorisation from a JobAdder account.
How Hookdeck Can Help: Hookdeck's request history shows exactly which events arrive, so a gap is easy to spot.
A credential you can't read back
The Problem: Your receiver rejects every notification after a deploy, and you can't check what JobAdder is sending.
Why It Happens: The authorization value isn't returned by the Webhook API, and JobAdder sends it unchanged, so a missing Bearer or Basic prefix in your stored copy breaks the comparison.
Workarounds:
- Store the exact string you sent when creating the webhook, prefix included.
- If in doubt, set a new value with
PUTand update your receiver at the same time.
How Hookdeck Can Help: Hookdeck's JobAdder source compares the whole Authorization value you paste. Hookdeck answers JobAdder with a 200 either way, but a notification with a missing or wrong value is recorded as failed verification and never reaches your service, and you can see what was received.
Best practices
Use a random token, not a password
The authorization example is a Basic credential, but nothing requires one. A long random bearer value avoids reusing a real username and password.
Deduplicate on eventId
JobAdder warns about duplicates. Store eventId before processing. See our guide to webhook idempotency.
Filter application status changes
Use eventFilters to limit jobapplication_ to the statuses you act on.
Watch webhook status
Check for Suspended or Failed webhooks and re-enable them once the cause is fixed.
Acknowledge fast and process asynchronously
Store the notification and respond, then call the JobAdder API for details. See why to process webhooks asynchronously.
Conclusion
JobAdder sends webhook notifications as JSON POST requests, authenticated only by the authorization string you set through the Webhook API. Store that string exactly, compare the whole header in constant time, deduplicate on eventId, and keep an eye on webhook status, because JobAdder doesn't document how it retries or when it gives up.
Hookdeck Event Gateway checks the header, stores every notification, and delivers it to your integration with retries and replay.
Get started with Hookdeck for free and handle JobAdder webhooks reliably in minutes.