Webhooks
Webhooks let Nanaade notify your application when something happens, instead of you polling. Register an HTTPS endpoint, choose the events you care about, and Nanaade sends a signed JSON POST to it each time one occurs.
Events
| Event | When it fires | data |
|---|---|---|
resume.analysis.completed | A résumé analysis finished | The same body returned by POST /resumes/analyze |
candidate.deleted | A candidate was erased via DELETE /candidates/{id} | { external_candidate_id, previously_known, deleted } |
pathway.completed / pathway.failed | A career pathway finished generating, or gave up | The pathway resource (see Career pathways) |
webhook.test | You called POST /webhooks/{id}/test | { message, endpoint_id, request_id } |
Register an endpoint
Requires the webhooks:manage scope. The URL must be https on a publicly reachable host.
curl https://backend.nanaade.ai/v1/webhooks \
-H "Authorization: Bearer $NANAADE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://api.partnerapp.com/nanaade/webhooks",
"events": ["resume.analysis.completed", "candidate.deleted"],
"description": "Production receiver"
}'
The response includes a secret (whsec_…). It is shown only once — store it in your secrets manager; you need it to verify signatures. Rotate it any time with POST /webhooks/{id}/rotate-secret (update your receiver first).
What you receive
POST /nanaade/webhooks HTTP/1.1
Content-Type: application/json
User-Agent: Nanaade-Webhooks/1.0
X-Nanaade-Signature: t=1758196921,v1=5c1a…e9f0
X-Nanaade-Event-Id: evt_8620ab540d00b96ababa6703
X-Nanaade-Event-Type: resume.analysis.completed
X-Nanaade-Delivery-Id: 6aad2ccd706b633638020606
{ "id": "evt_8620ab540d00b96ababa6703", "type": "resume.analysis.completed", "created_at": "2026-09-18T12:22:01.249Z", "data": { … } }
Respond with any 2xx within 10 seconds. Do the real work asynchronously.
Verify the signature
v1 is HMAC-SHA256(secret, "<t>.<raw request body>") in hex. Compute it over the raw bytes you received — do not re-serialise the JSON — and compare in constant time. Reject requests whose t is more than five minutes from your clock, and use X-Nanaade-Event-Id to ignore duplicates: retries reuse the same event id.
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.NANAADE_WEBHOOK_SECRET!;
app.post("/nanaade/webhooks", express.raw({ type: "application/json" }), (req, res) => {
const header = String(req.header("X-Nanaade-Signature") || "");
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const timestamp = Number(parts.t);
if (!parts.v1 || Math.abs(Date.now() / 1000 - timestamp) > 300) return res.status(400).end();
const expected = crypto.createHmac("sha256", SECRET).update(`${timestamp}.${req.body}`).digest("hex");
const valid = expected.length === parts.v1.length &&
crypto.timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(parts.v1, "hex"));
if (!valid) return res.status(400).end();
const event = JSON.parse(req.body.toString("utf8"));
// Acknowledge first, then process event.type / event.data on your own queue.
res.status(200).end();
handleEvent(event);
});
Retries and failures
If your endpoint does not return a 2xx, Nanaade retries after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours. After five failed attempts the delivery is marked failed; you can inspect it and re-send it from GET /webhooks/{id}/deliveries and POST …/deliveries/{delivery_id}/retry. An endpoint that fails 50 deliveries in a row is disabled automatically — fix the receiver, then re-enable it with PATCH /webhooks/{id} and { "status": "active" }.
Delivery history is kept for 30 days.
Testing
POST /webhooks/{id}/test queues a webhook.test event (subscribe to it when creating the endpoint). Try it in the sandbox; the delivery list shows each attempt's status code and timing.