QudraQudra Docs
Get API key

Webhooks

Webhooks are how Qudra pushes events back to you as they happen — when a new seeker matches your job, when an embedding finishes processing, or when a partner-managed job is mutated. Every delivery is HMAC-signed so you can verify authenticity in constant time.

Growth & Scale only

Webhooks require the Growth or Scale plan. On Sandbox you'll get 403 partner.forbidden when calling /v1/partner/webhooks. Partner pricing is under revision — see Plans for current terms.

Events

| Event | Fired when | |--------------------|--------------------------------------------------------| | job.created | Embedding pipeline finished; job is now match_ready. | | job.matched | A high-confidence seeker match was added to the job. | | job.updated | Title, description, or status changed. | | job.deleted | Job was soft-deleted. |

Register

POST/v1/partner/webhooks
curl -X POST https://api.qudrah.io/v1/partner/webhooks \
  -H "Authorization: Bearer $QUDRA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url":    "https://your-app.example.com/webhooks/qudra",
    "events": ["job.created", "job.matched"]
  }'

Response:

{
  "id":     "wh_01HXY…",
  "url":    "https://your-app.example.com/webhooks/qudra",
  "events": ["job.created", "job.matched"],
  "secret": "whsec_a1b2c3…",
  "is_active": true,
  "created_at": "2026-05-21T08:14:22.123Z"
}

Save the secret now

The secret is shown once. Store it alongside your API key in your secrets manager. You'll need it to verify every incoming delivery.

Delivery shape

Every delivery is a POST with the following headers:

POST /webhooks/qudra HTTP/1.1
Content-Type: application/json
X-Qudra-Event: job.matched
X-Qudra-Delivery-Id: dlv_01HXZ…
X-Qudra-Signature: t=1747700400,v1=4f3a…
X-Qudra-Timestamp: 1747700400
User-Agent: Qudra-Webhooks/1.0

Body:

{
  "id":   "evt_01HXZ…",
  "type": "job.matched",
  "created_at": "2026-05-21T08:14:22.123Z",
  "data": {
    "job_id":   "job_01HXY…",
    "seeker_id":"seek_01HXZ…",
    "score":    0.91,
    "qudra_url":"https://qudrah.io/jobs/senior-backend-engineer-01hxy…"
  }
}

Verify

The signature header has the form t=<timestamp>,v1=<hex_hmac>. The HMAC is SHA-256 of ${timestamp}.${rawBody} using your webhook secret as the key.

Node.js (express)

import { createHmac, timingSafeEqual } from 'node:crypto';
 
function verify(rawBody: string, header: string, secret: string): boolean {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const expected = createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex');
  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(parts.v1, 'hex');
  return a.length === b.length && timingSafeEqual(a, b);
}
 
app.post('/webhooks/qudra', express.raw({ type: 'application/json' }), (req, res) => {
  const ok = verify(req.body.toString(), req.header('X-Qudra-Signature')!, process.env.QUDRA_WEBHOOK_SECRET!);
  if (!ok) return res.status(401).end();
  const event = JSON.parse(req.body.toString());
  // …handle event…
  res.status(200).end();
});

Or with the SDK:

import { QudraClient } from '@qudra/sdk';
 
const qudra = new QudraClient({ apiKey: process.env.QUDRA_API_KEY! });
 
app.post(
  '/webhooks/qudra',
  express.raw({ type: 'application/json' }),
  qudra.webhooks.expressMiddleware(process.env.QUDRA_WEBHOOK_SECRET!),
  (req, res) => {
    // req.qudraEvent is typed
    res.status(200).end();
  },
);

Python (Flask / FastAPI)

import hmac, hashlib
from qudra import Client
 
def verify(raw_body: bytes, header: str, secret: str) -> bool:
    parts    = dict(p.split('=') for p in header.split(','))
    expected = hmac.new(secret.encode(), f"{parts['t']}.{raw_body.decode()}".encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts['v1'])

PHP

function qudra_verify(string $rawBody, string $header, string $secret): bool {
    parse_str(str_replace(',', '&', $header), $parts);
    $expected = hash_hmac('sha256', $parts['t'] . '.' . $rawBody, $secret);
    return hash_equals($expected, $parts['v1']);
}

Retry & failure handling

  • We expect a 2xx response within 10 seconds.
  • On non-2xx or timeout, we retry with exponential backoff: 1m → 5m → 30m → 2h → 12h (5 attempts).
  • After 5 consecutive failures, the webhook is auto-disabled and we email your partner admins.
  • Each delivery carries a unique X-Qudra-Delivery-Id — store it to deduplicate retries.

List, update, delete

GET/v1/partner/webhooks
PATCH/v1/partner/webhooks/:id
DELETE/v1/partner/webhooks/:id

Standard CRUD — PATCH accepts partial updates of url, events, and is_active.