Qudra Partner API — Vol. 01
QudraPartner API 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

EventFired when
job.createdEmbedding pipeline finished; job is now match_ready.
job.matchedA high-confidence seeker match was added to the job.
job.updatedTitle, description, or status changed.
job.deletedJob was soft-deleted.

Register

POST/v1/partner/webhooksv1Bearer

Register a delivery URL and event filter. Returns the webhook record + a one-time secret you must persist.

Request body

FieldTypeDescription
url
stringURLHTTPS endpoint we POST deliveries to. Must respond 2xx within 10s.
events
array<string>Event types to subscribe to.
Enumjob.createdjob.matchedjob.updatedjob.deleted
is_active
booleanSet to false to register but pause deliveries.
Defaulttrue
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 — 201 Created

{
  "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"
}
FieldTypeDescription
id
stringwh_<ulid>Webhook identifier — use it for update / delete.
url
stringURLThe HTTPS endpoint you registered.
events
array<string>Subscribed event types.
secret
stringwhsec_…HMAC signing secret. Shown once at creation — store it now.
is_active
booleanWhether deliveries are currently being sent.
created_at
stringISO-8601Server creation time.

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

Delivery headers

FieldTypeDescription
X-Qudra-Event
stringThe event type (`job.created`, …).
X-Qudra-Delivery-Id
stringdlv_<ulid>Unique per delivery. Store + dedupe to make retries idempotent.
X-Qudra-Signature
stringHMAC signature in `t=<timestamp>,v1=<hex>` form.
X-Qudra-Timestamp
integerepoch secondsSame value as `t=` inside the signature. Reject deliveries > 5 minutes old.

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…"
  }
}
FieldTypeDescription
id
stringevt_<ulid>Event identifier — stable across retries of the same event.
type
enumEvent type.
Enumjob.createdjob.matchedjob.updatedjob.deleted
created_at
stringISO-8601Event timestamp.
data
objectEvent-specific payload. Shape varies per `type`.

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/webhooksv1Bearer

List all webhooks owned by your partner.

PATCH/v1/partner/webhooks/:idv1Bearer

Partial update of `url`, `events`, or `is_active`.

DELETE/v1/partner/webhooks/:idv1Bearer

Permanently remove the webhook. Returns 204 No Content.