Skip to content
Webhooks

Hear about every hiring step. Signed, retried and logged.

SeekIQ posts a small JSON event to your HTTPS endpoint when something happens in the pipeline. Check the signature, store the event id and answer with a 2xx.

Set up an endpoint

  1. Open Settings > API and webhooks as an owner and add an endpoint: its HTTPS address and the events it should receive.
  2. Copy the signing secret (it starts with whsec_). It is shown once.
  3. Press Send test. A ping event arrives, signed like every other event.

You can do the same from code with POST /webhooks and a key that has webhooks:manage. POST /webhooks

Event types

Choose the events per endpoint. New event types may be added; ignore types you do not handle.

EventWhen it is sent
application.createdA new application, from any source: careers site, embed, API, jobs inbox or referral
application.stage_changedAn application moved between stages, by a person or an auto-advance rule
interview.completedThe candidate finished an AI interview
scorecard.readyThe AI scorecard finished, with every citation checked
decision.madeA person recorded advance, hold or reject
decision.undoneA person undid a decision inside its five minute undo window
offer.sentAn approved offer went to the candidate
offer.signedThe candidate signed the offer
offer.declinedThe candidate declined the offer, with a reason code
document.sentAn agreement or contract went out for signature
document.signedOne party signed a document; complete is true once everyone has
document.declinedA party declined a document, which closes it
candidate.data_requestThe candidate asked to see or delete their data or for a human review. If you hold a copy, you have duties too
pingSent only to one endpoint when you press Send test or call the test endpoint.

Payload

Every event has the same envelope. data holds ids and a short summary, not full personal data; fetch the resource with the API when you need more.

JSON
{
  "id": "evt_01JB7Y2K9T3V4W5X6Y7Z8A9B",
  "type": "application.stage_changed",
  "api_version": "v1",
  "created_at": "2026-10-26T09:30:00Z",
  "org_id": "org_01JB2M0Q",
  "data": { ... }
}

Headers

Each delivery carries these headers:

HTTP
Content-Type: application/json
User-Agent: SeekIQ-Webhooks/1.0 (+https://seekiq.io/developers/webhooks)
SeekIQ-Signature: t=1760779800,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
SeekIQ-Event-Id: evt_01JB7Y2K9T3V4W5X6Y7Z8A9B
SeekIQ-Event-Type: application.stage_changed
SeekIQ-Delivery-Id: 0193f1c2-7a4e-7b1d-9c3e-5f8a2b6d4e10
SeekIQ-SignatureTimestamp and one or two HMAC signatures
SeekIQ-Event-IdThe event, the same on every retry
SeekIQ-Event-TypeThe event type, also in the body
SeekIQ-Delivery-IdThis attempt; a retry gets a new one

Verify the signature

The signature is a hex HMAC-SHA256 of the timestamp, a dot and the raw request body, keyed with the endpoint secret. Compute it over the exact bytes you received before any JSON parsing, then compare in constant time. Reject a timestamp more than five minutes from your clock.

Node.js

verify.js
import crypto from 'node:crypto';

/** True when one of the v1 signatures matches and the timestamp is recent. */
export function verifySeekIQSignature(rawBody, header, secret, toleranceSec = 300) {
  if (!header) return false;
  const parts = header.split(',').map((p) => p.trim().split('='));
  const t = Number(parts.find(([k]) => k === 't')?.[1]);
  const sigs = parts.filter(([k]) => k === 'v1').map(([, v]) => v);
  if (!t || sigs.length === 0) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - t) > toleranceSec) return false;

  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest();
  return sigs.some((s) => {
    const got = Buffer.from(s, 'hex');
    return got.length === expected.length && crypto.timingSafeEqual(got, expected);
  });
}
server.js
import express from 'express';

const app = express();
const seen = new Set(); // use your database in production

// Read the raw body: the signature covers the exact bytes SeekIQ sent.
app.post('/seekiq/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
  const raw = req.body.toString('utf8');
  if (!verifySeekIQSignature(raw, req.get('SeekIQ-Signature'), process.env.SEEKIQ_WEBHOOK_SECRET)) {
    return res.sendStatus(400);
  }
  const event = JSON.parse(raw);
  if (!seen.has(event.id)) {
    seen.add(event.id);
    // queue your work here, then answer quickly
  }
  res.sendStatus(200);
});

Python

verify.py
import hashlib
import hmac
import time


def verify_seekiq_signature(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    """True when one of the v1 signatures matches and the timestamp is recent."""
    if not header:
        return False
    parts = [p.strip().split("=", 1) for p in header.split(",") if "=" in p]
    t = next((v for k, v in parts if k == "t"), "")
    sigs = [v for k, v in parts if k == "v1"]
    if not t.isdigit() or not sigs:
        return False
    if abs(int(time.time()) - int(t)) > tolerance:
        return False
    expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected, s) for s in sigs)
app.py
import os
from flask import Flask, abort, request

app = Flask(__name__)


@app.post("/seekiq/webhooks")
def seekiq_webhook():
    raw = request.get_data()  # the exact bytes, before JSON parsing
    if not verify_seekiq_signature(raw, request.headers.get("SeekIQ-Signature", ""),
                                   os.environ["SEEKIQ_WEBHOOK_SECRET"]):
        abort(400)
    event = request.get_json()
    # skip event["id"] if you have handled it, otherwise queue the work
    return "", 200

Secret rotation

Rotating a secret (in Settings or with the rotate-secret endpoint) keeps the old one working for 24 hours. During that time the header carries two v1 values, the new secret first. Accept the event if either matches.

Retries and backoff

A delivery succeeds on any 2xx within 10 seconds. Anything else, including a redirect, a timeout or a refused connection, is retried.

Retry schedule
After 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours, each give or take 10 percent: 8 attempts over about two days.

After 3 days of failures in a row the endpoint is turned off and workspace owners get an email. Turn it back on in Settings or with PATCH active: true.

Answer quickly: store the event, return 200 and do the work in the background.

Idempotency

The same event can arrive more than once, for example when your 200 was lost on the way back. Every attempt carries the same SeekIQ-Event-Id and body, with a new SeekIQ-Delivery-Id. Store the event id and skip one you have already handled.

Ordering

Events are not guaranteed to arrive in order, and retries can overtake newer events. Use created_at on the event and updated_at on the resource, or fetch the current state with the API.

Testing deliveries

Send test posts a ping event to one endpoint. The delivery log in Settings shows every attempt with its status code, time taken, the first 1 KB of your answer and the next retry, and any delivery can be sent again. The same is available from code:

curl
# Send a ping to one endpoint
curl -X POST "https://app.seekiq.io/api/v1/webhooks/whk_01JBCJ7W/test" \
  -H "Authorization: Bearer $SEEKIQ_API_KEY"

# Read the delivery log (pending, succeeded or failed)
curl "https://app.seekiq.io/api/v1/webhooks/whk_01JBCJ7W/deliveries?status=failed" \
  -H "Authorization: Bearer $SEEKIQ_API_KEY"

# Send one delivery again
curl -X POST "https://app.seekiq.io/api/v1/webhooks/whk_01JBCJ7W/deliveries/dlv_01JBDK8X/redeliver" \
  -H "Authorization: Bearer $SEEKIQ_API_KEY"

A redelivery is one attempt with a new delivery id and is not retried automatically.

Endpoint addresses

Endpoint addresses must use https and reach the public internet. Private, loopback and other reserved addresses are refused when you save and checked again before each delivery. Redirects are not followed. For local work, use a tunnel with a public HTTPS address.

Zapier, Make and n8n

There is no SeekIQ app in the Zapier or Make directories yet. Their catch hooks work today:

  1. Create a catch hook: Webhooks by Zapier with Catch Hook, a Custom webhook in Make or a Webhook node in n8n.
  2. Add its address as an endpoint in Settings > API and webhooks and pick the events.
  3. The tool sees type and data as fields. Check SeekIQ-Signature if the tool can run code; otherwise keep the hook address private, as it is the only secret.
  4. For actions, use the tool's HTTP step with a service key that has only the scopes the flow needs, and an Idempotency-Key on writes.

Slack and Microsoft Teams messages need no tool: Settings > Integrations posts them directly.

Event payloads

The data each event carries. The sample values are made up; field names and types come from the OpenAPI file.

ping

Sent only to one endpoint when you press Send test or call the test endpoint.

  • messagestringRequired
Example payload
{
  "id": "evt_01JB7Y2K9T3V4W5X6Y7Z8A9B",
  "type": "ping",
  "api_version": "v1",
  "created_at": "2026-10-26T09:30:00Z",
  "org_id": "org_01JB2M0Q",
  "data": {
    "message": "Hello from SeekIQ"
  }
}

application.created

A new application, from any source: careers site, embed, API, jobs inbox or referral

  • application_idIdRequired
  • job_idIdRequired
  • candidate_idIdRequired
  • sourceApplicationSourceRequired
    careers_siteembedapiemail_inboxreferralsourcedjob_board
  • originstring | nullRequired

    Host origin for embed applications

  • answersApplicationAnswer[]Required

    The apply form answers, as on the Application.

Example payload
{
  "id": "evt_01JB7Y2K9T3V4W5X6Y7Z8A9B",
  "type": "application.created",
  "api_version": "v1",
  "created_at": "2026-10-26T09:30:00Z",
  "org_id": "org_01JB2M0Q",
  "data": {
    "application_id": "app_01JB7X4M2Q8Z",
    "job_id": "job_01JB7X4M2Q8Z",
    "candidate_id": "cand_01JB7X4M2Q8Z",
    "source": "careers_site",
    "origin": "string",
    "answers": [
      {
        "question_id": "string",
        "label": {},
        "type": "string",
        "value": "string"
      }
    ]
  }
}

application.stage_changed

An application moved between stages, by a person or an auto-advance rule

  • application_idIdRequired
  • job_idIdRequired
  • from_stageStageRequired
  • to_stageStageRequired
  • actorActorRequired
  • reasonstring | nullRequired
Example payload
{
  "id": "evt_01JB7Y2K9T3V4W5X6Y7Z8A9B",
  "type": "application.stage_changed",
  "api_version": "v1",
  "created_at": "2026-10-26T09:30:00Z",
  "org_id": "org_01JB2M0Q",
  "data": {
    "application_id": "app_01JB7X4M2Q8Z",
    "job_id": "job_01JB7X4M2Q8Z",
    "from_stage": {
      "id": "stg_01JB7X4M2Q8Z",
      "type": "applied",
      "name": "Phone screen",
      "order": 1
    },
    "to_stage": {
      "id": "stg_01JB7X4M2Q8Z",
      "type": "applied",
      "name": "Phone screen",
      "order": 1
    },
    "actor": {
      "type": "user",
      "id": "usr_01JB7X4M2Q8Z"
    },
    "reason": "Strong on customer empathy"
  }
}

interview.completed

The candidate finished an AI interview

  • interview_idIdRequired
  • application_idIdRequired
  • kindstringRequired

    Always ai today; human interviews are scheduled meetings with feedback, not sessions.

    aihuman
  • completed_atstring (date-time)Required
Example payload
{
  "id": "evt_01JB7Y2K9T3V4W5X6Y7Z8A9B",
  "type": "interview.completed",
  "api_version": "v1",
  "created_at": "2026-10-26T09:30:00Z",
  "org_id": "org_01JB2M0Q",
  "data": {
    "interview_id": "int_01JB7X4M2Q8Z",
    "application_id": "app_01JB7X4M2Q8Z",
    "kind": "ai",
    "completed_at": "2026-10-26T09:30:00Z"
  }
}

scorecard.ready

The AI scorecard finished, with every citation checked

  • application_idIdRequired
  • scorecard_versionintegerRequired
  • open_flagsintegerRequired
Example payload
{
  "id": "evt_01JB7Y2K9T3V4W5X6Y7Z8A9B",
  "type": "scorecard.ready",
  "api_version": "v1",
  "created_at": "2026-10-26T09:30:00Z",
  "org_id": "org_01JB2M0Q",
  "data": {
    "application_id": "app_01JB7X4M2Q8Z",
    "scorecard_version": 1,
    "open_flags": 0
  }
}

decision.made

A person recorded advance, hold or reject

  • decision_idIdRequired
  • application_idIdRequired
  • outcomeDecisionOutcomeRequired
    advanceholdreject
  • decided_byActorRequired
Example payload
{
  "id": "evt_01JB7Y2K9T3V4W5X6Y7Z8A9B",
  "type": "decision.made",
  "api_version": "v1",
  "created_at": "2026-10-26T09:30:00Z",
  "org_id": "org_01JB2M0Q",
  "data": {
    "decision_id": "dec_01JB7X4M2Q8Z",
    "application_id": "app_01JB7X4M2Q8Z",
    "outcome": "advance",
    "decided_by": {
      "type": "user",
      "id": "app_01JB7X4M2Q8Z"
    }
  }
}

decision.undone

A person undid a decision inside its five minute undo window

  • decision_idIdRequired
  • application_idIdRequired
  • outcomeDecisionOutcomeRequired
    advanceholdreject
  • undone_byActorRequired
Example payload
{
  "id": "evt_01JB7Y2K9T3V4W5X6Y7Z8A9B",
  "type": "decision.undone",
  "api_version": "v1",
  "created_at": "2026-10-26T09:30:00Z",
  "org_id": "org_01JB2M0Q",
  "data": {
    "decision_id": "dec_01JB7X4M2Q8Z",
    "application_id": "app_01JB7X4M2Q8Z",
    "outcome": "advance",
    "undone_by": {
      "type": "user",
      "id": "app_01JB7X4M2Q8Z"
    }
  }
}

offer.sent

An approved offer went to the candidate

  • offer_idIdRequired
  • application_idIdRequired
  • candidate_idIdRequired
  • statusOfferStatusRequired
    draftpending_approvalapprovedsentsigneddeclinedexpiredwithdrawn
  • document_sha256string | null

    sha256 of the offer letter PDF as sent (offer.sent and offer.signed)

  • signed_sha256string

    sha256 of the signed copy (offer.signed)

  • expires_onstring (date)

    Last day the offer can be signed (offer.sent)

  • reasonstring

    The candidate's decline reason code (offer.declined)

Example payload
{
  "id": "evt_01JB7Y2K9T3V4W5X6Y7Z8A9B",
  "type": "offer.sent",
  "api_version": "v1",
  "created_at": "2026-10-26T09:30:00Z",
  "org_id": "org_01JB2M0Q",
  "data": {
    "offer_id": "off_01JB7X4M2Q8Z",
    "application_id": "app_01JB7X4M2Q8Z",
    "candidate_id": "cand_01JB7X4M2Q8Z",
    "status": "draft"
  }
}

offer.signed

The candidate signed the offer

  • offer_idIdRequired
  • application_idIdRequired
  • candidate_idIdRequired
  • statusOfferStatusRequired
    draftpending_approvalapprovedsentsigneddeclinedexpiredwithdrawn
  • document_sha256string | null

    sha256 of the offer letter PDF as sent (offer.sent and offer.signed)

  • signed_sha256string

    sha256 of the signed copy (offer.signed)

  • expires_onstring (date)

    Last day the offer can be signed (offer.sent)

  • reasonstring

    The candidate's decline reason code (offer.declined)

Example payload
{
  "id": "evt_01JB7Y2K9T3V4W5X6Y7Z8A9B",
  "type": "offer.signed",
  "api_version": "v1",
  "created_at": "2026-10-26T09:30:00Z",
  "org_id": "org_01JB2M0Q",
  "data": {
    "offer_id": "off_01JB7X4M2Q8Z",
    "application_id": "app_01JB7X4M2Q8Z",
    "candidate_id": "cand_01JB7X4M2Q8Z",
    "status": "draft"
  }
}

offer.declined

The candidate declined the offer, with a reason code

  • offer_idIdRequired
  • application_idIdRequired
  • candidate_idIdRequired
  • statusOfferStatusRequired
    draftpending_approvalapprovedsentsigneddeclinedexpiredwithdrawn
  • document_sha256string | null

    sha256 of the offer letter PDF as sent (offer.sent and offer.signed)

  • signed_sha256string

    sha256 of the signed copy (offer.signed)

  • expires_onstring (date)

    Last day the offer can be signed (offer.sent)

  • reasonstring

    The candidate's decline reason code (offer.declined)

Example payload
{
  "id": "evt_01JB7Y2K9T3V4W5X6Y7Z8A9B",
  "type": "offer.declined",
  "api_version": "v1",
  "created_at": "2026-10-26T09:30:00Z",
  "org_id": "org_01JB2M0Q",
  "data": {
    "offer_id": "off_01JB7X4M2Q8Z",
    "application_id": "app_01JB7X4M2Q8Z",
    "candidate_id": "cand_01JB7X4M2Q8Z",
    "status": "draft"
  }
}

document.sent

An agreement or contract went out for signature

  • document_idIdRequired
  • kindstringRequired
    offeremployment_agreementndacontractor_agreementpolicyother
  • titlestringRequired
  • statusstringRequired
    sentcompleteddeclinedvoidedexpired
  • candidate_idId | nullRequired
  • application_idId | nullRequired
  • offer_idId | nullRequired
  • original_sha256string | nullRequired

    sha256 of the PDF as sent

  • party_rolestring

    The party that signed or declined (document.signed and document.declined)

    candidatecompanywitness
  • completeboolean

    True once every party has signed (document.signed)

  • signed_sha256string

    sha256 of the copy this signature produced, each copy with its own fingerprint (document.signed)

  • final_sha256string | null

    sha256 of the final copy with every signature and the certificate once complete, else null (document.signed)

Example payload
{
  "id": "evt_01JB7Y2K9T3V4W5X6Y7Z8A9B",
  "type": "document.sent",
  "api_version": "v1",
  "created_at": "2026-10-26T09:30:00Z",
  "org_id": "org_01JB2M0Q",
  "data": {
    "document_id": "app_01JB7X4M2Q8Z",
    "kind": "offer",
    "title": "Customer support lead",
    "status": "sent",
    "candidate_id": "cand_01JB7X4M2Q8Z",
    "application_id": "app_01JB7X4M2Q8Z",
    "offer_id": "off_01JB7X4M2Q8Z",
    "original_sha256": "string"
  }
}

document.signed

One party signed a document; complete is true once everyone has

  • document_idIdRequired
  • kindstringRequired
    offeremployment_agreementndacontractor_agreementpolicyother
  • titlestringRequired
  • statusstringRequired
    sentcompleteddeclinedvoidedexpired
  • candidate_idId | nullRequired
  • application_idId | nullRequired
  • offer_idId | nullRequired
  • original_sha256string | nullRequired

    sha256 of the PDF as sent

  • party_rolestring

    The party that signed or declined (document.signed and document.declined)

    candidatecompanywitness
  • completeboolean

    True once every party has signed (document.signed)

  • signed_sha256string

    sha256 of the copy this signature produced, each copy with its own fingerprint (document.signed)

  • final_sha256string | null

    sha256 of the final copy with every signature and the certificate once complete, else null (document.signed)

Example payload
{
  "id": "evt_01JB7Y2K9T3V4W5X6Y7Z8A9B",
  "type": "document.signed",
  "api_version": "v1",
  "created_at": "2026-10-26T09:30:00Z",
  "org_id": "org_01JB2M0Q",
  "data": {
    "document_id": "app_01JB7X4M2Q8Z",
    "kind": "offer",
    "title": "Customer support lead",
    "status": "sent",
    "candidate_id": "cand_01JB7X4M2Q8Z",
    "application_id": "app_01JB7X4M2Q8Z",
    "offer_id": "off_01JB7X4M2Q8Z",
    "original_sha256": "string"
  }
}

document.declined

A party declined a document, which closes it

  • document_idIdRequired
  • kindstringRequired
    offeremployment_agreementndacontractor_agreementpolicyother
  • titlestringRequired
  • statusstringRequired
    sentcompleteddeclinedvoidedexpired
  • candidate_idId | nullRequired
  • application_idId | nullRequired
  • offer_idId | nullRequired
  • original_sha256string | nullRequired

    sha256 of the PDF as sent

  • party_rolestring

    The party that signed or declined (document.signed and document.declined)

    candidatecompanywitness
  • completeboolean

    True once every party has signed (document.signed)

  • signed_sha256string

    sha256 of the copy this signature produced, each copy with its own fingerprint (document.signed)

  • final_sha256string | null

    sha256 of the final copy with every signature and the certificate once complete, else null (document.signed)

Example payload
{
  "id": "evt_01JB7Y2K9T3V4W5X6Y7Z8A9B",
  "type": "document.declined",
  "api_version": "v1",
  "created_at": "2026-10-26T09:30:00Z",
  "org_id": "org_01JB2M0Q",
  "data": {
    "document_id": "app_01JB7X4M2Q8Z",
    "kind": "offer",
    "title": "Customer support lead",
    "status": "sent",
    "candidate_id": "cand_01JB7X4M2Q8Z",
    "application_id": "app_01JB7X4M2Q8Z",
    "offer_id": "off_01JB7X4M2Q8Z",
    "original_sha256": "string"
  }
}

candidate.data_request

The candidate asked to see or delete their data or for a human review. If you hold a copy, you have duties too

  • request_idIdRequired
  • candidate_idIdRequired
  • kindstringRequired
    accessdeletehuman_reviewhuman_interview
  • due_atstring (date-time)Required
Example payload
{
  "id": "evt_01JB7Y2K9T3V4W5X6Y7Z8A9B",
  "type": "candidate.data_request",
  "api_version": "v1",
  "created_at": "2026-10-26T09:30:00Z",
  "org_id": "org_01JB2M0Q",
  "data": {
    "request_id": "app_01JB7X4M2Q8Z",
    "candidate_id": "cand_01JB7X4M2Q8Z",
    "kind": "access",
    "due_at": "2026-10-26T09:30:00Z"
  }
}