POST the applicant to /api/careers/applications from your server with a JSON body of jobId, name and email, and your company apply key in an Authorization: Bearer header. HTTP 201 means a new application, HTTP 200 means that person already had a card for that job.
The key authorizes writes for your whole company, so it must stay server-side. Never call this endpoint from browser JavaScript.
Integration in five steps
Steps one and two are in the dashboard. Steps three to five are on your own website.
One request from your backend. Your careers page never leaves your domain.
- Create your jobsAdd each open role in the dashboard. An application has to land against an open job, so the jobs must exist before anything is posted. Closing a job makes the careers API reject applications for it.
- Mint an apply keyOn the Pipeline screen, issue a company apply key. It looks like
iwk_1a2b3c4d…and is shown exactly once, because only its hash is stored. Copy it straight into your server environment as something likeINTERVIEWWATCH_APPLY_KEY. - Read your job ids from the APICall
GET /api/careers/jobsand render your careers page from the result. Hardcoding ids works, but reading them means a closed or renamed role never leaves a dead form behind. - Post each applicant from your form handlerOne request per submission, with
jobId,nameandemail. Examples for several stacks are below. - Show the candidate a confirmationTreat 201 and 200 alike in your UI. A 200 simply means they had already applied to that role, which is not something to make them worry about.
The three endpoints
Base URL is https://app.interviewwatch.com. The key goes in Authorization: Bearer iwk_…, or in X-InterviewWatch-Key if your CMS or form tool cannot set the Authorization header.
| Method and path | Purpose | Notes |
|---|---|---|
GET /api/careers/jobs | List your open jobs | Returns id, name, role and a display label. Open jobs only, since a closed one is a listing nobody can apply to. |
POST /api/careers/applications | Submit one applicant | JSON body: jobId, name, email. 201 on create, 200 on repeat. |
POST /api/careers/applications/import?jobId=… | Bulk import a CSV | Multipart file upload. Header row with name and email, plus an optional jobId column per row. |
There is no resume field, no cover letter and no custom questions, because a candidate record here is a name and an email address. If your form collects more, keep it on your side or in your email.
Code examples in four stacks
curl, to test the key
curl -X POST https://app.interviewwatch.com/api/careers/applications \ -H "Authorization: Bearer $INTERVIEWWATCH_APPLY_KEY" \ -H "Content-Type: application/json" \ -d '{"jobId":"3f1c…","name":"Ada Lovelace","email":"[email protected]"}'
Next.js route handler
// app/api/apply/route.ts — runs on the server, so the key stays private export async function POST(request: Request) { const form = await request.formData() const res = await fetch('https://app.interviewwatch.com/api/careers/applications', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.INTERVIEWWATCH_APPLY_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ jobId: form.get('jobId'), name: form.get('name'), email: form.get('email'), }), }) // 201 = new application, 200 = they had already applied. Both are fine. if (!res.ok) { const { error } = await res.json().catch(() => ({})) return Response.json({ ok: false, error }, { status: 400 }) } return Response.json({ ok: true }) }
PHP or WordPress form hook
$response = wp_remote_post('https://app.interviewwatch.com/api/careers/applications', [ 'headers' => [ 'Authorization' => 'Bearer ' . getenv('INTERVIEWWATCH_APPLY_KEY'), 'Content-Type' => 'application/json', ], 'body' => wp_json_encode([ 'jobId' => $job_id, 'name' => $name, 'email' => $email, ]), ]);
Framer, Webflow or a static site
Bulk import from a CSV
curl -X POST "https://app.interviewwatch.com/api/careers/applications/import?jobId=3f1c…" \ -H "Authorization: Bearer $INTERVIEWWATCH_APPLY_KEY" \ -F "[email protected]" # applicants.csv # name,email # Ada Lovelace,[email protected] # Grace Hopper,[email protected] # # Response: { "created": 2, "duplicates": 0, "failed": 0, "rows": [ … ] } # Every row is reported separately, with a line number and reason for failures.
The same import is available as a file picker in the dashboard, so an admin can bulk-load without touching a terminal. A per-row jobId column overrides the query parameter, which is how you load several roles from one file.
What the API returns
// 201 Created, or 200 OK when the person already had a card for this job { "applicationId": "9b7e…", "candidateId": "41af…", "jobId": "3f1c…", "stage": "Applied", "source": "CareersApi", "created": true, // false on a repeat submission "active": true // false if they were already Hired or Rejected for this job }
| Status | Meaning | What your form should do |
|---|---|---|
201 | New application created | Show the confirmation message |
200 | They already had a card for this job | Show the same confirmation. Do not tell them off for reapplying |
400 | Missing or invalid jobId, name or email, or the job is closed. Body carries a human-readable error | Show the field-level message and let them correct it |
401 | Missing, wrong or revoked apply key | Alert your team. Do not surface this to the candidate |
429 | Rate limited. A Retry-After header says how long to wait | Queue and retry after the stated delay |
On active: false: the candidate was already Hired or Rejected for that role, and the existing card is left exactly as it was. Reopening it for a fresh round is a deliberate recruiter action in the dashboard, so a repeat application can never quietly undo a decision someone made on purpose.
Rate limits and caps
Generous for a careers page, tight enough to absorb a bot storm on your apply form.
Retry-After headerRows and values that exceed a cap are rejected rather than truncated, so a mangled payload is visible to you instead of silently stored in a chopped-off form.
Keeping your apply key secure
Rules
- Server-side only. Never in page JavaScript, a mobile app or a public repo
- Environment variable, not a config file in version control
- One key per company. Rotate rather than share it around
- Rotate on staff change or on any suspicion of exposure
- Watch the last-used timestamp in the dashboard to confirm the integration is live before revoking an old key
How it is stored
Only a SHA-256 hash and a short display prefix are kept, so a database dump hands out no working keys and "I lost the key" is answered by rotating rather than by recovery.
Minting a new key replaces the old one. Deleting the key makes the careers API reject everything for your company, which is the fastest way to stop an integration you no longer trust.
The key authorizes creating applications only. It cannot read your pipeline, move candidates or schedule interviews.
More on how the platform handles data: security overview and privacy policy.
Careers page integration: frequently asked questions
How do I send applications from my careers page into the ATS?
jobId, name and email to /api/careers/applications from your website's backend, with your apply key in an Authorization: Bearer header. 201 means a new application, 200 means that person already had a card for that job.Can I call it from browser JavaScript?
Do you host a careers page for us?
What if someone submits the form twice?
created: false instead of making a duplicate card. Candidates are deduplicated by email across your whole company.Can one person apply to several roles?
What are the rate limits?
Retry-After header.How do I rotate or revoke the key?
Can I post a resume or custom questions?
Connect your careers page
Issue a key, add the call to your form handler, and watch cards appear on the board marked as career-page applications.
Open the dashboard See the built-in ATS