Webhooks

Receive real-time job lifecycle events at your HTTPS endpoint instead of polling. HMAC-signed, retried on failure, auto-disabled when broken.

Webhooks push job lifecycle events to your endpoint in near-real-time instead of forcing you to poll. Available on Starter, Growth, Professional, and Custom plans, with these per-plan caps on active subscriptions:

PlanActive webhook subscriptions
Starter1
Growth3
Professional5
Customagreed per contract

Why webhooks

Polling the Pull API works well for batch refresh, but it's wasteful when you only want to react to changes. Webhooks let you stay event-driven:

  • job.created: a job we haven't seen before is published
  • job.updated: a tracked job changed (title, salary, status, etc.)
  • job.closed: a job's listing stopped being live
  • job.deleted: a job was removed (your store should delete it too)

Webhook deliveries are at-least-once. Your handler must be idempotent. See Dedupe below.

Quickstart

  1. Open Webhooks in your dashboard and click New Subscription.
  2. Give it a name, paste your HTTPS endpoint, pick the event types and an optional filter, and submit.
  3. Copy the signing secret on the one-time reveal screen. Jobven never shows it again.
  4. Deploy your endpoint with the verifier recipe below.
  5. Click Send test, from either the create-success modal or the subscription detail page, to fire a deterministic webhook.test event at your endpoint. The subscription stays in pending_verification until your endpoint returns 2xx, at which point it flips to active and starts receiving real job events.
New subscriptions start in pending_verification and do not receive any real job events until a Send test succeeds. This prevents an empty or broken endpoint from being flooded the moment you save.

Subscription lifecycle

StatusMeaningHow to leave it
pending_verificationCreated (or freshly re-enabled). Not in the dispatch pool, so no events are delivered.Send a test event from the dashboard. A 2xx response flips it to active automatically.
activeReceiving live events that match the filter, plus every job.closed and job.deleted.Disable from the dashboard, or hit the failure thresholds (auto-disable).
disabled_by_userYou paused it. No events are delivered.Re-enable from the dashboard. This re-routes through pending_verification, so you'll need to send a fresh test first.
disabled_due_to_failuresAuto-disabled after 50 consecutive failures or 24h with no successful delivery.Hit Re-enable on the detail page. You'll get the listedAfter value for the gap so you can replay it through the Pull API, and the subscription returns to pending_verification until you send another successful test.

The verification gate intentionally applies to every state transition that resumes delivery, including re-enable. The endpoint may have changed between when you paused and when you resumed. Re-testing before flipping back to active means a broken endpoint can't silently start consuming quota on retries.

A failed test event leaves the status untouched (still pending or disabled). Test failures do not count toward the auto-disable thresholds and do not bump the streak counters.

Request shape

Every delivery is a POST to your URL with these headers:

POST /your/webhook/path HTTP/1.1
Content-Type: application/json
User-Agent: Jobven-Webhook/1.0
Jobven-Event-Id: 8e3f5c44-1cd1-4a40-bb4f-9d4f95c54c1d
Jobven-Event-Type: job.created
Jobven-Subscription-Id: 7c2f8f7a-d8ab-4d4a-9a09-71b4e6dffb8a
Jobven-Delivery-Attempt: 1
Jobven-Signature: t=1715520000,v1=2e3...

The body is a JSON envelope:

{
  "id": "8e3f5c44-1cd1-4a40-bb4f-9d4f95c54c1d",
  "type": "job.created",
  "createdAt": 1715520000,
  "subscriptionId": "7c2f8f7a-d8ab-4d4a-9a09-71b4e6dffb8a",
  "deliveryAttempt": 1,
  "data": {
    "job": { /* … see /docs/reference/jobs for the full shape */ }
  }
}

See Event reference for the payload shape of each event type.

Signature verification

Always verify the signature before trusting a payload. Compute HMAC-SHA256(t + "." + raw_body) with your subscription's signing secret and compare to the v1= value in Jobven-Signature. Reject if the timestamp drifts more than 300 seconds (replay protection).

import crypto from 'node:crypto'

const TOLERANCE_SECONDS = 300

export function verifyJobvenWebhook(
  rawBody: string,
  signatureHeader: string,
  secret: string,
): boolean {
  const parts = Object.fromEntries(
    signatureHeader.split(',').map((p) => p.split('=')),
  )
  const t = Number(parts.t)
  const v1 = parts.v1
  if (!t || !v1) return false

  const drift = Math.abs(Math.floor(Date.now() / 1000) - t)
  if (drift > TOLERANCE_SECONDS) return false

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${t}.${rawBody}`)
    .digest('hex')

  const a = Buffer.from(expected, 'hex')
  const b = Buffer.from(v1, 'hex')
  if (a.length !== b.length) return false

  return crypto.timingSafeEqual(a, b)
}

Always use the raw request body as bytes for HMAC. JSON parsing changes whitespace and breaks verification.

Dedupe on Jobven-Event-Id

Webhook deliveries are at-least-once. Persist the Jobven-Event-Id for each event you've processed and skip duplicates. The header is the same UUID across all retry attempts for the same event.

Retries and timeouts

A delivery is considered successful when your endpoint returns a 2xx status within 10 seconds. Otherwise we retry on the following schedule:

AttemptDelay from previous
230 seconds
32 minutes
410 minutes
530 minutes

After the fifth failed attempt we give up. We retry on 5xx, network timeouts, and 429. Other 4xx responses (400, 404, 410, 422) are treated as terminal: fix your endpoint, don't ignore them.

Auto-disable, re-enable, and backfill

To protect both sides, Jobven auto-disables a subscription that hits 50 consecutive failed deliveries or goes 24 hours with no successful delivery. The dashboard surfaces a banner and a re-enable flow.

When you re-enable, Jobven returns backfill.listedAfter, the moment the subscription was disabled. Pull GET /v1/public/jobs?listedAfter= from that value to recover the jobs listed during the gap, and skip ids you already have: the replay runs up to the present, so it overlaps the jobs the resumed stream delivers. It finds new and reopened jobs; to refresh a job you already hold, fetch it by id. See Sync Only New Jobs. The subscription itself returns to pending_verification: send another successful test, and the live webhook stream resumes from that moment. Pull-backfill plus a successful test together cover the entire gap with no replay of stored events.

Quota accounting

  • Your plan's monthly allowance is measured in jobs delivered, shared between the Pull API and webhooks. A successful job.created or job.updated delivery counts one job, because it carries a full job record. There is no daily quota.
  • job.closed and job.deleted are free, and they keep arriving even when your account is over its limit, so the jobs you already hold do not go stale.
  • Test events do not count.
  • Failed delivery attempts do not count, only successful ones.
  • When you are over your limit, new and updated jobs are dropped, not queued, and are not re-sent when the period resets. Backfill the gap through the Pull API with listedAfter.

Running out of quota

Once you reach your monthly cap, Pull API requests return 429 and job.created and job.updated events are dropped rather than queued. job.closed and job.deleted are free and keep arriving, so the jobs you already hold stay current.

This is not a pause. A dropped event is not stored and is not re-sent when your quota resets or when you upgrade, so any job that is created or updated while you are over the cap will never arrive as a webhook. Unlike auto-disable, there is no backfill window returned for it.

If you go over, treat the gap the same way you would a disable window: replay it through the Pull API with listedAfter set to when you went over, once your quota is available again, and skip ids you already have. See Sync Only New Jobs.

We email you at 80%, 90%, and 100% of your cap. The 80% and 90% warnings are the ones worth acting on: by the time the 100% email arrives, events are already being dropped.A 429 response carries Retry-After and X-RateLimit-Reset headers, and a resetAt timestamp in the body, telling you when your quota next resets.

Filter scope

Subscriptions accept a structured filter that mirrors a subset of the public Pull API. Allowed keys:

remoteType, experienceLevel, employmentType, industry, skills, language, country, state, city, minSalary, maxSalary, hasVisaSponsorship, hasRelocation.

See Jobs API reference for the semantics of each filter parameter. An empty filter delivers every active job event. It works, but it burns quota aggressively. Always narrow.

job.closed and job.deleted ignore your filter. You receive them whenever a job closes or is removed, even if that job never matched.A closure or deletion is owed to anyone who might be holding the record, and we can't work out who that is from the job's final state: a job can reach you under one set of attributes and close under another. Sending it to everyone is the only way to guarantee nobody is left serving a job that is gone. Match these events on id: two jobs can share an apply URL. If you don't recognise the id, ignore the event; there's nothing to clean up.job.created and job.updated are filtered as described above.

Next