Interviews run with the InterviewWatch agent get a signed integrity report on the candidate card. See how verification works →
Careers page integration

Careers page integration guide

We deliberately host no careers page and no apply form. Your site keeps its own page, its own branding and its own URLs, and posts each applicant to the apply API. This is the whole integration: an endpoint, a key, and a JSON body with three fields.

Endpoint
POST /api/careers/applications
Auth
Bearer apply key
Body
jobId, name, email
Duplicates
Idempotent per job and email
Bulk
CSV, 500 rows per file
Time to integrate
Under an hour
Short answer

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.

Setup

Integration in five steps

Steps one and two are in the dashboard. Steps three to five are on your own website.

Your careers page stays on your domain Your backend holds the apply key InterviewWatch the pipeline applicant submits the form POST /api/careers/applications 201 created, or the existing card re-submitting never duplicates

One request from your backend. Your careers page never leaves your domain.

  1. 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.
  2. 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 like INTERVIEWWATCH_APPLY_KEY.
  3. Read your job ids from the APICall GET /api/careers/jobs and 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.
  4. Post each applicant from your form handlerOne request per submission, with jobId, name and email. Examples for several stacks are below.
  5. 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.
Endpoints

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 pathPurposeNotes
GET /api/careers/jobsList your open jobsReturns id, name, role and a display label. Open jobs only, since a closed one is a listing nobody can apply to.
POST /api/careers/applicationsSubmit one applicantJSON body: jobId, name, email. 201 on create, 200 on repeat.
POST /api/careers/applications/import?jobId=…Bulk import a CSVMultipart file upload. Header row with name and email, plus an optional jobId column per row.
Name and email are all we take.

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.

Examples

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

Do not put the key in the page. Static site builders can submit a form to any URL, which makes it tempting to point the form straight at the API. Do not: the key would be readable by anyone viewing source. Send the form to a small serverless function instead (Vercel, Netlify, Cloudflare Workers all work), store the key as an environment variable there, and have that function make the call. The function is about fifteen lines, as above.

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.

Responses and errors

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
}
StatusMeaningWhat your form should do
201New application createdShow the confirmation message
200They already had a card for this jobShow the same confirmation. Do not tell them off for reapplying
400Missing or invalid jobId, name or email, or the job is closed. Body carries a human-readable errorShow the field-level message and let them correct it
401Missing, wrong or revoked apply keyAlert your team. Do not surface this to the candidate
429Rate limited. A Retry-After header says how long to waitQueue 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.

Limits

Rate limits and caps

Generous for a careers page, tight enough to absorb a bot storm on your apply form.

Applications60 per minute per company key
Applications, per IP20 per minute per client IP address
Jobs endpoint120 requests per minute
CSV import5 requests per minute
CSV rows500 per file
CSV size512 KB per file
Name length200 characters maximum
Email length320 characters maximum, and it must be a valid address
Throttled responseHTTP 429 with a Retry-After header

Rows 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.

Security

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.

FAQ

Careers page integration: frequently asked questions

How do I send applications from my careers page into the ATS?
POST 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?
No. The key authorizes writes for your whole company, so it has to stay on the server. Point your form at a backend route or a small serverless function and keep the key in an environment variable there.
Do you host a careers page for us?
No, deliberately. Your page keeps your domain, your branding and your URLs, and only applicant data comes to us. That is also why leaving is cheap: there are no vendor-hosted job URLs to redirect and no SEO to rebuild.
What if someone submits the form twice?
The call is idempotent per job and email. The second submission returns the existing application with created: false instead of making a duplicate card. Candidates are deduplicated by email across your whole company.
Can one person apply to several roles?
Yes. They become one candidate with one card per job, and their interview history stays on that single identity rather than being split across duplicates.
What are the rate limits?
60 applications per minute per company key and 20 per minute per client IP. The jobs endpoint allows 120 per minute, CSV import 5 per minute with 500 rows and 512 KB per file. Throttled requests return 429 with a Retry-After header.
How do I rotate or revoke the key?
Mint a new key from the Pipeline screen, which replaces the old one, or delete the key to make the careers API reject everything for your company. Check the last-used timestamp first to be sure which key your deployment is actually using.
Can I post a resume or custom questions?
No. The API takes a name and an email, because that is all a candidate record holds. Keep anything else your form collects on your own side.

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
Keep reading