Developers
API & integration guide
This is a documented integration. Nothing is connected until an organisation administrator creates an API key under API & webhooks and installs it on the calling platform's server.
Authentication
Send Authorization: Bearer wsp_live_… from your server only. The organisation is derived from the key — any organisation ID in a request body is ignored. Keys are stored hashed; rotate by creating a new key and revoking the old one.
- Base URL:
https://your-app.example/api/public/v1(the paths below omit/publicfor brevity:/api/v1/x→/api/public/v1/x). - Rate limit: 120 requests per minute per key (HTTP 429).
Idempotency-Keyheader on any POST: repeats return the original response with headeridempotent-replayed: true.- Errors:
{"error":{"code","message","details"}}with 400/401/404/409/422/429/500.
Endpoints
/api/v1/uploadsUpload a CV (multipart field `file`, or JSON `{filename, content_base64}`). PDF or .txt, ≤5 MB. Safety-checked before storage. Returns `cv_reference`.
/api/v1/assessmentsCreate a draft assessment. Returns `id` and a `review_url` for the recruiter.
/api/v1/assessments/{id}/questions/generateDraft personalised questions (tailored mode). Requires recruiter-reviewed CV extraction when a CV is attached.
/api/v1/assessments/{id}/approveApprove questions so an invitation can be issued. Normally done by the recruiter in the dashboard.
/api/v1/assessments/{id}Status, external references, invitations and report versions.
/api/v1/invitationsBody `{assessment_id}`. Returns a single-use candidate `url` (shown once) and `expires_at`.
/api/v1/invitations/{id}Invitation status: active, used, expired or revoked.
/api/v1/invitations/{id}/revokeRevoke an active invitation.
/api/v1/reports/{id}Report metadata, versions and `approved` flag. No report text.
/api/v1/reports/{id}/download-linkShort-lived (5 min) signed PDF URL. Only for recruiter-approved reports (409 otherwise).
Create assessment body
{
"candidate_name": "Jordan Lee", // required
"candidate_email": "jordan@example.com",
"external_candidate_id": "rpo-cand-123",
"external_application_id": "rpo-app-456", // unique per organisation
"external_role_id": "rpo-role-789", // shared role question sets are reused per role
"mode": "standard" | "tailored",
"cv_reference": "<from /uploads>",
"client_name": "Acme Ltd",
"company_name": "Acme Ltd",
"company_brief": "…", // recruiter must approve before generation
"job_title": "…", "job_description": "…", "responsibilities": "…",
"essential_criteria": "…", "manager_priorities": "…",
"send_mode": "link" // "email" falls back to link until email is configured
}Example (server-side Node)
const API = "https://your-app.example/api/public/v1";
const h = { Authorization: `Bearer ${process.env.WSP_API_KEY}`, "Content-Type": "application/json" };
const a = await fetch(`${API}/assessments`, { method: "POST", headers: { ...h, "Idempotency-Key": "app-456-create" },
body: JSON.stringify({ candidate_name: "Jordan Lee", external_application_id: "rpo-app-456", mode: "standard" }) }).then(r => r.json());
const inv = await fetch(`${API}/invitations`, { method: "POST", headers: { ...h, "Idempotency-Key": "app-456-invite" },
body: JSON.stringify({ assessment_id: a.id }) }).then(r => r.json());
// send inv.url to the candidate
// later, after report.approved webhook:
const { url } = await fetch(`${API}/reports/${reportId}/download-link`, { method: "POST", headers: h }).then(r => r.json());
const pdf = await fetch(url).then(r => r.arrayBuffer()); // save against the applicationcurl -X POST https://your-app.example/api/public/v1/uploads \
-H "Authorization: Bearer $WSP_API_KEY" -F "file=@cv.pdf"Webhooks
Events: assessment.started, assessment.completed, report.ready, report.approved, report.failed, invitation.expired. Payloads contain IDs and references only — never CV contents, answers or report text.
POST <your endpoint>
x-wsp-event-id: evt_…
x-wsp-event-type: report.approved
x-wsp-signature: t=1767225600,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>
{ "id": "evt_…", "type": "report.approved", "created_at": "…", "organisation_id": "…",
"data": { "assessment_id": "…", "report_id": "…", "external_candidate_id": "…",
"external_application_id": "…", "external_role_id": "…" } }Verifying signatures (Node)
import crypto from "node:crypto";
export function verify(rawBody, header, secret, toleranceSec = 300) {
const { t, v1 } = Object.fromEntries(header.split(",").map(p => p.split("=")));
if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSec) return false; // replay protection
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}- Respond 2xx within 8 seconds. Failures retry with exponential backoff (2, 4, 8, 16, 32 minutes; 6 attempts), then are marked failed. Recruiters can retry from the delivery log.
- Duplicates: deliveries are at-least-once. Store processed
x-wsp-event-idvalues and ignore repeats. - Events may arrive out of order — use
GET /assessments/{id}for the authoritative status.
Connecting Recruitment Partners OS
- In this app, an organisation administrator creates an API key (API & webhooks) and copies it.
- In RPO's backend, store it as a server secret (e.g.
WSP_API_KEY) and the base URL asWSP_API_URL. Never send it to the browser. - Add RPO's HTTPS webhook URL here; store the signing secret in RPO as
WSP_WEBHOOK_SECRETand verify every delivery. - From the candidate/application record, RPO uploads the CV (optional), creates the assessment with its external IDs, and opens
review_urlfor the recruiter. - The recruiter reviews CV extraction and approves questions in this dashboard; RPO then calls
POST /invitationsand sends the link. - RPO updates status from webhooks. On
report.approved, RPO requests a download link, fetches the PDF and saves it against the matching external application ID.
This app never reads or writes the RPO database.