Set callback_url when you submit a job and rafiq-api POSTs the result to it once the job reaches a terminal state, so you don't have to poll GET .../{id}.
The payload shape depends on the job type and outcome:
// Transcription — completed
{ "id": "…", "status": "completed", "transcript": { ... } }
// Transcription — failed
{ "id": "…", "status": "failed", "error": { "code": "...", "message": "..." } }
// Document — completed
{ "id": "…", "status": "completed", "pages": 12, "formats": ["md"] }
// Document — failed
{ "id": "…", "status": "failed", "error": { "code": "...", "message": "..." } }
A completed document job's payload carries only the page count and formats, never the extracted text — fetch that separately with GET /api/v1/documents/{id}?format=md|txt|json.
A signed delivery carries an X-Rafiq-Signature header: the HMAC-SHA256 hex digest of the exact raw request body, computed with your organization's webhook signing secret.
X-Rafiq-Signature: <hex hmac-sha256>Verify against the exact bytes of the body as received. Parsing the JSON and re-serializing it before verifying produces a different string and will fail intermittently.
Deliveries with no signing secret configured
If your organization has no signing secret, the delivery still goes out — just without the X-Rafiq-Signature header, never signed with an empty key. A signature computed from an empty secret is something anyone could reproduce, so no header is a more honest signal than a fake one: treat an unsigned delivery as unverified.
Delivery is retried up to 3 times, waiting 1 second then 2 seconds between attempts, if your endpoint doesn't return a successful response. After the last attempt fails, rafiq-api stops trying.
Manage your signing secret in Settings → WebhooksVerify a delivery (Node.js)
const crypto = require("crypto");
// rawBody must be the exact, unparsed request body — the same bytes rafiq-api
// signed. Verifying JSON.parse(rawBody) re-serialized will fail intermittently:
// key order and whitespace are not guaranteed to round-trip identically.
function isValidSignature(rawBody, signatureHeader, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
return (
signatureHeader.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected))
);
}