Browse documentation
Public API

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

EventWhen it firesdata
resume.analysis.completedA résumé analysis finishedThe same body returned by POST /resumes/analyze
candidate.deletedA candidate was erased via DELETE /candidates/{id}{ external_candidate_id, previously_known, deleted }
pathway.completed / pathway.failedA career pathway finished generating, or gave upThe pathway resource (see Career pathways)
webhook.testYou 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.