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/webhookscurl -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.0Body:
{
"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
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/webhooks/v1/partner/webhooks/:id/v1/partner/webhooks/:idStandard CRUD — PATCH accepts partial updates of url, events, and is_active.