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
/v1/partner/webhooksv1BearerRegister a delivery URL and event filter. Returns the webhook record + a one-time secret you must persist.
Request body
| Field | Type | Description |
|---|---|---|
url | stringURL | HTTPS endpoint we POST deliveries to. Must respond 2xx within 10s. |
events | array<string> | Event types to subscribe to. Enum job.createdjob.matchedjob.updatedjob.deleted |
is_active | boolean | Set to false to register but pause deliveries. Default true |
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"
}| Field | Type | Description |
|---|---|---|
id | stringwh_<ulid> | Webhook identifier — use it for update / delete. |
url | stringURL | The HTTPS endpoint you registered. |
events | array<string> | Subscribed event types. |
secret | stringwhsec_… | HMAC signing secret. Shown once at creation — store it now. |
is_active | boolean | Whether deliveries are currently being sent. |
created_at | stringISO-8601 | Server 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.0Delivery headers
| Field | Type | Description |
|---|---|---|
X-Qudra-Event | string | The event type (`job.created`, …). |
X-Qudra-Delivery-Id | stringdlv_<ulid> | Unique per delivery. Store + dedupe to make retries idempotent. |
X-Qudra-Signature | string | HMAC signature in `t=<timestamp>,v1=<hex>` form. |
X-Qudra-Timestamp | integerepoch seconds | Same 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…"
}
}| Field | Type | Description |
|---|---|---|
id | stringevt_<ulid> | Event identifier — stable across retries of the same event. |
type | enum | Event type. Enum job.createdjob.matchedjob.updatedjob.deleted |
created_at | stringISO-8601 | Event timestamp. |
data | object | Event-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
2xxresponse 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
/v1/partner/webhooksv1BearerList all webhooks owned by your partner.
/v1/partner/webhooks/:idv1BearerPartial update of `url`, `events`, or `is_active`.
/v1/partner/webhooks/:idv1BearerPermanently remove the webhook. Returns 204 No Content.