Gareth Wilson Gareth Wilson

Guide to JobAdder Webhooks: Features and Best Practices

Published


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

FeatureDetails
ManagementWebhook API: POST /webhooks, GET, PUT and DELETE /webhooks/{webhookId}
Scopesoffline_access plus each event's read scopes. Partner action events also need partner_ui_action
MethodPOST
EnvelopeapiVersion, event, eventId, eventDate, eventUser, eventData, then event details
AuthenticationOptional authorization value, sent verbatim as Authorization. No signature
FilterseventFilters for jobapplication_status_changed, by statusId or active status
StatusEnabled, Disabled, Suspended, and Failed (with failedAt)
Ordering and duplicatesOrder not guaranteed; duplicates possible
Retries, timeout, source IPsNot documented

Common events

AreaEvents
Jobsjob_status_changed, job_invoice_sent, job_partner_action
Job adsjobad_posted, jobad_expired, jobad_partner_action
Candidatescandidate_status_changed, candidate_updated, candidate_skill_updated, candidate_partner_action
Applicationsjobapplication_status_changed, jobapplication_partner_action
Placementsplacement_approved, placement_status_changed, placement_partner_action
Companiescompany_status_changed, company_updated, company_address_updated, company_partner_action
Contactscontact_status_changed, contact_updated, contact_partner_action
Interviews and eventsinterview_scheduled, interview_evaluation_submitted, event_scheduled, event_interview_evaluation_submitted
Foldersfolder_job_added, folder_candidate_added, folder_placement_added and the matching _removed events for jobs, candidates, applications, placements, companies and contacts, plus folder_updated
Otheruser_suspended, sms_sent, opportunity_stage_changed

JobAdder keeps adding events: its release notes list opportunity_stage_changed, 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 Authorization header, 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 Authorization value, 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 if status changes or failedAt is 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 eventDate with 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 PUT and 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_status_changed 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.


Gareth Wilson

Gareth Wilson

Product Marketing

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