API & webhooks
A REST API over everything in the product and signed webhooks for everything that happens in it. Keys are made in Settings, scoped to one workspace, and revocable. No partner programme, no sales call, no waiting list.
| METHOD | PATH | WHAT IT DOES |
|---|---|---|
| GET | /api/v1/candidates | Every candidate, newest first. Paged. |
| GET | /api/v1/candidates/{id} | One candidate with their applications and current stage. |
| POST | /api/v1/candidates | Put somebody in the database. Refuses a duplicate email and hands back the id of the record that already exists.· needs a write key |
| GET | /api/v1/jobs | Open and closed roles, with the client each belongs to. |
| POST | /api/v1/jobs | Open a role against a client. The client is checked against your workspace, and an end client is refused as the company to invoice.· needs a write key |
| GET | /api/v1/clients | The client accounts. |
| POST | /api/v1/clients | Add a client account. Refuses a duplicate company name and hands back the id of the record that already exists.· needs a write key |
| GET | /api/v1/applications | Who is on which role, and what stage they are at. |
| GET | /api/v1/placements | Confirmed placements, with start dates. |
| GET | /api/v1/invoices | Invoices with their status and amounts. Needs a key that may see money. |
| GET | /api/v1/webhooks | The subscriptions this workspace has. |
| POST | /api/v1/webhooks | Subscribe a URL to one or more events.· needs a write key |
| DELETE | /api/v1/webhooks/{id} | Stop a subscription.· needs a write key |
The questions everybody asks in the first minute.
A key per workspace, sent as `Authorization: Bearer sk_live_…`. Keys are made in Settings → API & webhooks and shown once. Two scopes: read, and write which implies read. There is no third level worth the screen it would need.
You can create a candidate, a client and a job, and manage webhooks. You cannot move somebody to Offered, raise an invoice or place anybody. The three you can create are the ones an integration actually asks for — a form on your own site, a spreadsheet in Zapier, an HRMS pushing applicants, a client system opening a requirement — and each is “put this record in the database”, where the rules are ours and a script cannot get them wrong. The rest is judgement, and an API that lets a script move somebody to Offered is an API that will move somebody to Offered by accident. Commercial terms are excluded on purpose: fee model, payment terms and the vendor chain decide who gets billed what, and no integration has a reason to set them.
600 requests a minute per key. Every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`, so a client can slow down before it is told to; a refusal is a 429 with `Retry-After`. The limit exists to stop a loop with no delay in it, which is the commonest integration bug there is.
`?page=` and `?pageSize=` on every list, with `?q=` for a text search. Responses carry `rows`, `total`, `page` and `pageSize` — the same shape everywhere, so a client written against one list works against all of them.
One shape: `{ "error": "…" }` with a real HTTP status, and `detail` where there is something useful to say. Never a 200 with a failure inside it.
Delivered as a POST with a JSON body and an `X-StaffingCRM-Signature` header: `sha256(timestamp.body)` with your endpoint's secret, the same scheme Stripe and GitHub use. Verify it before you trust the body. Failed deliveries are retried with a backoff, and the attempts are visible in Settings.
Both work today with nothing extra to install: a webhook subscription is the trigger, and the REST endpoints are the actions. There is no separate connector because there is nothing for one to do.
The path carries the version. `v1` will not change shape under you — a field may be added, never removed or renamed. Anything that has to break becomes `v2` beside it.
Deliberately not every event in the product. A feed full of noise is one nobody keeps subscribed to.
candidate.appliedSomebody applied through the careers pagecandidates.importedA batch of candidates was importedapplication.submittedA candidate was submitted to a clientapplication.stage_changedAn application moved stageapplication.rejectedAn application was rejectedinterview.scheduledAn interview was bookedinterview.feedbackInterview feedback was recordedclient.decisionA client shortlisted or rejected in the portaldeal.wonA deal was wondeal.lostA deal was lostplacement.extendedA placement was extendedtimesheet.submittedA timesheet was sent to the clienttimesheet.submitted_by_contractorA contractor sent their hoursinvoice.raised_from_timesheetsAn invoice was raised from timesheetscurl https://your-instance.example/api/v1/candidates \
-H "Authorization: Bearer sk_live_…"
# create one
curl https://your-instance.example/api/v1/candidates \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"name":"Priya Sharma","email":"priya@example.in","skills":["React"]}'The host is your own instance — this product is delivered, not shared, so there is no api.staffingcrm.com holding everybody’s data behind one door.