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 /public for brevity: /api/v1/x → /api/public/v1/x).
  • Rate limit: 120 requests per minute per key (HTTP 429).
  • Idempotency-Key header on any POST: repeats return the original response with header idempotent-replayed: true.
  • Errors: {"error":{"code","message","details"}} with 400/401/404/409/422/429/500.

Endpoints

POST
/api/v1/uploads

Upload a CV (multipart field `file`, or JSON `{filename, content_base64}`). PDF or .txt, ≤5 MB. Safety-checked before storage. Returns `cv_reference`.

POST
/api/v1/assessments

Create a draft assessment. Returns `id` and a `review_url` for the recruiter.

POST
/api/v1/assessments/{id}/questions/generate

Draft personalised questions (tailored mode). Requires recruiter-reviewed CV extraction when a CV is attached.

POST
/api/v1/assessments/{id}/approve

Approve questions so an invitation can be issued. Normally done by the recruiter in the dashboard.

GET
/api/v1/assessments/{id}

Status, external references, invitations and report versions.

POST
/api/v1/invitations

Body `{assessment_id}`. Returns a single-use candidate `url` (shown once) and `expires_at`.

GET
/api/v1/invitations/{id}

Invitation status: active, used, expired or revoked.

POST
/api/v1/invitations/{id}/revoke

Revoke an active invitation.

GET
/api/v1/reports/{id}

Report metadata, versions and `approved` flag. No report text.

POST
/api/v1/reports/{id}/download-link

Short-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 application
curl -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-id values and ignore repeats.
  • Events may arrive out of order — use GET /assessments/{id} for the authoritative status.

Connecting Recruitment Partners OS

  1. In this app, an organisation administrator creates an API key (API & webhooks) and copies it.
  2. In RPO's backend, store it as a server secret (e.g. WSP_API_KEY) and the base URL as WSP_API_URL. Never send it to the browser.
  3. Add RPO's HTTPS webhook URL here; store the signing secret in RPO as WSP_WEBHOOK_SECRET and verify every delivery.
  4. From the candidate/application record, RPO uploads the CV (optional), creates the assessment with its external IDs, and opens review_url for the recruiter.
  5. The recruiter reviews CV extraction and approves questions in this dashboard; RPO then calls POST /invitations and sends the link.
  6. 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.