Skip to main content
Developer guide

Build with IT Manager Jobs (Demo)

Programmatic access to job posting, candidate search, and pipeline events for ATS partners (Bullhorn, Vincere, Mercury, JobAdder, custom in-house tools). Stable v1 contract, OpenAPI-described, signed webhooks, no extra licence fee.

1. Sign up 2. Mint an API key 3. Authenticate 4. Make your first call Job Posting API Applications API Candidate API CV Search API Webhooks Rate limits Errors Versioning Install the CLI OpenAPI & SDKs Support

1. Sign up

You need an employer account on IT Manager Jobs (Demo) before you can access the API. The same account that posts jobs through the website is the one your ATS integration will run against โ€” credits, contracts, and pipeline data are all shared between the two.

1

Create the account

Create an employer account if you don't already have one. Self-serve, no credit card needed to explore.

2

Pick a package or contract

API credits are the same as portal credits โ€” buy a job-posting bundle from our pricing page, or talk to sales about an unlimited contract for higher-volume use.

3

Be an employer admin

Only users with the EmployerAdmin role can mint API keys. Have your account owner do step 4, or have them invite you as an admin from the Team page.

2. Mint an API key

Sign in to the employer portal as an admin, then open API keys under your account.

1

Click "Create key"

Give the key a descriptive name โ€” usually the integration that'll use it ("Bullhorn production", "internal HR Slack bot"). One key per integration is the right pattern; revoking a single integration is then a single click.

2

Pick the scopes

JobPosting covers /api/v1/jobs/...; Applications covers the recruiter applicant pipeline (/api/v1/applications/..., plus tags, notes and interviewer scorecards); CvSearch covers /api/v1/candidates/.... Pick only what the integration needs โ€” endpoints reject keys that don't carry the right scope, and a narrow scope limits the blast radius if a key leaks.

3

Optionally set an expiry

Production integrations usually leave this blank. Internal scripts and time-bounded data migrations should set a date โ€” expired keys auto-disable.

4

Copy the key, then wait for approval

The plaintext pk_live_โ€ฆ value is shown exactly once on creation. Store it in your secrets manager (AWS Secrets Manager, HashiCorp Vault, 1Password, GitHub Actions secrets, etc.) before closing the dialog. We only store a hash; we cannot recover the key for you.

Most keys for your own job-posting flow are auto-approved instantly — you can call the API immediately. Keys with the CvSearch scope, and keys for accounts without an active contract or credit balance, queue for one-business-day platform review. You'll get an email the moment the key is approved; until then API calls return 401 Unauthorized — that's expected, not a key error.

Trusted partners (signed integrations, repeat customers) can be flagged for instant approval on every key — talk to us if your integration is shipping to multiple environments and you don't want each one to wait. Lost a key? Revoke it on the API keys page and mint a new one.

3. Authenticate

Send the key as a Bearer token (preferred) or as a dedicated X-Api-Key header. Both work; pick whichever fits your HTTP client cleanest.

Authorization: Bearer pk_live_AbCdEf...
# or
X-Api-Key: pk_live_AbCdEf...
  • Keys are tenant-scoped to the employer that minted them โ€” a key from one employer can never read another employer's data.
  • Each key carries a fixed set of scopes chosen at creation time. Endpoints reject keys missing the required scope with 403 Forbidden.
  • Revoked, expired, or unknown keys return 401 Unauthorized immediately, with no body content.
  • The LastUsedAt timestamp is updated on every successful call, visible on the API keys management page.

4. Make your first call

The simplest verification is to list your jobs โ€” empty, but a 200 confirms auth + scope are right:

curl -H "Authorization: Bearer pk_live_โ€ฆ" \
     https://demo.it-manager-jobs.com/api/v1/jobs

# {"total":0,"page":1,"pageSize":50,"items":[]}

If you got 401: the key is wrong or revoked. If you got 403: the key was minted without the JobPosting scope โ€” mint a new one with both scopes, or with the right one for your integration.

Job Posting API Scope: JobPosting

MethodPathDescriptionCost
GET/api/v1/jobsList your jobs (paginated, filterable by status)Free
GET/api/v1/jobs/{id}Single job + full descriptionFree
POST/api/v1/jobsCreate a draft jobFree
PUT/api/v1/jobs/{id}Patch any subset of mutable fieldsFree
POST/api/v1/jobs/{id}/publishDraft โ†’ Active. Sets postedDate and expiryDate.1 Job Posting credit
DELETE/api/v1/jobs/{id}Deactivate (soft-close). Existing applications are preserved.Free

Example โ€” create then publish

curl -X POST https://demo.it-manager-jobs.com/api/v1/jobs \
  -H "Authorization: Bearer pk_live_โ€ฆ" \
  -H "Content-Type: application/json" \
  -d '{
        "title": "Senior Platform Engineer",
        "reference": "PL-001",
        "description": "Build and run the platform...",
        "city": "London",
        "country": "United Kingdom",
        "employmentType": "FullTime",
        "remoteType": "HybridRemote",
        "applyMethod": "EmailApply",
        "applyEmailAddress": "[email protected]"
      }'

# 201 Created
# {"id":1234,"status":"Draft", ... }

curl -X POST https://demo.it-manager-jobs.com/api/v1/jobs/1234/publish \
  -H "Authorization: Bearer pk_live_โ€ฆ" \
  -H "Content-Type: application/json" \
  -d '{"expiryDate":"2026-06-01T00:00:00Z"}'

# 200 OK โ€” status now "Active"

Applications API Scope: Applications

Read the applicant pipeline for jobs you own, fetch the AI suitability assessment computed when the application landed, move candidates through stages, and write the ATS surface โ€” tags, recruiter notes, and interviewer scorecards. Tenant-scoped to the key's employer; cross-tenant calls return 404.

MethodPathDescriptionCost
GET/api/v1/jobs/{jobId}/applicationsPaginated list of applications for one of your jobs (filterable by status)Free
GET/api/v1/applications/{id}Single application โ€” candidate name, email, headline, contact details, status, screener answersFree
GET/api/v1/applications/{id}/ai-assessmentAI suitability score + verdict + rubric breakdown. Reads the persisted score; never re-runs the model.Free
POST/api/v1/applications/{id}/statusMove the application to a new stage (New, Screened, Shortlisted, Interviewing, Interviewed, Offered, Hired, Rejected, Withdrawn). Writes an audit-history row.Free
GET/api/v1/tagsYour employer's tag dictionary with per-tag usage countsFree
GET/api/v1/applications/{id}/tagsTags assigned to the candidate behind this applicationFree
POST/api/v1/applications/{id}/tagsAssign a tag (auto-created on first use; case-folded so "Hot" and "HOT" share one tag)Free
DELETE/api/v1/applications/{id}/tags/{label}Unassign a tag from the candidateFree
GET/api/v1/applications/{id}/notesRecruiter notes (application-scoped + candidate-level)Free
POST/api/v1/applications/{id}/notesCreate a note. Attributed to the user who minted the key.Free
PUT/api/v1/notes/{id}Edit a note. Only the original author can.Free
DELETE/api/v1/notes/{id}Soft-delete a note. Only the original author can.Free
GET/api/v1/applications/{id}/scorecardsAll interviewer scorecards on this applicationFree
PUT/api/v1/applications/{id}/scorecards/{stage}Upsert your scorecard for the given pipeline stage. Recommendation: StrongNo, No, Hold, Yes, StrongYes.Free
DELETE/api/v1/applications/{id}/scorecards/{stage}Delete your scorecard for the given stage.Free

Example โ€” list, read, move stage

curl -H "Authorization: Bearer pk_live_โ€ฆ" \
     "https://demo.it-manager-jobs.com/api/v1/jobs/1234/applications?status=New&pageSize=20"

curl -H "Authorization: Bearer pk_live_โ€ฆ" \
     https://demo.it-manager-jobs.com/api/v1/applications/9876/ai-assessment
# 200 OK โ€” { score, verdict, summary, rubricScores: [...] }

curl -X POST https://demo.it-manager-jobs.com/api/v1/applications/9876/status \
  -H "Authorization: Bearer pk_live_โ€ฆ" \
  -H "Content-Type: application/json" \
  -d '{ "status": "Shortlisted", "note": "Top of stack โ€” book a screen" }'

Note attribution. Notes and scorecards record an author user id. If the API key was minted without an associated user (rare โ€” sales-side mint), note / scorecard writes return 403 unattributable. Re-mint the key from your own account to enable those writes.

Candidate API Scope: Profile, Applications

Candidate-self surface โ€” read & update your own profile, list your own applications. Authenticated by a candidate-issued API key (separate from the recruiter key flow), minted from your candidate API keys page. Lets you wire up AI assistants (Claude, Copilot, Cursor) or the jbme CLI to drive your job-search workflow conversationally.

Isolation. Every candidate key only ever resolves to the owning candidate's own data โ€” there's no way for a key to see another candidate's profile or applications. Validated in a separate database table from recruiter keys; the auth schemes don't cross-pollinate.

MethodPathDescriptionCost
GET/api/v1/me/profileFull profile bundle โ€” identity, contact, socials, headline, summary, address, plus preferences, skills, languages, nationalities, employment history, education. Includes a 0-100 completeness score.Free
PUT/api/v1/me/profileAll-optional patch โ€” every field is optional, null means "leave alone". Covers identity, contact, socials, address, and the preferences subset (career status, salary expectations, notice period, job-type prefs). Returns the full bundle on success.Free
GET/api/v1/me/applicationsPaginated list of your own applications (filterable by status)Free
GET/api/v1/me/applications/{id}Single application โ€” job, employer, status, applied + updated timestamps, cover letter, external resume URLFree
POST/api/v1/me/applicationsApply to one job on the candidate's behalf. Scope Apply. Required body fields: jobId (one per call โ€” no array), confirm: true (a missing flag returns 400 confirmation_required). Same eligibility gates as the portal apply (consent freshness, screening-question requirements). Successful submissions surface to the recruiter with an "AI-assisted" badge.Free

Apply safeguards

The apply endpoint is the highest-risk surface in the candidate API because AI fan-out can spam-apply on the candidate's behalf. The platform enforces several guardrails server-side:

  • Explicit per-call confirmation. Every request body must carry "confirm": true. The MCP tool description spells out "you are applying on behalf of the candidate โ€” confirm with them first"; missing the flag means a clean 400 rather than a silent submission.
  • 10 successful applies per UTC day per candidate. Enforced across every key the candidate holds โ€” revoking and re-minting doesn't reset the counter, and an AI client running on two devices can't split traffic to exceed the cap. Hitting it returns 429 with a Retry-After header pointing at the next free slot.
  • One jobId per call. No array; no bulk-apply. Each application requires its own deliberate tool invocation.
  • Same consent gate as the portal. Candidates whose data-processing consent has lapsed are blocked here too โ€” the v1 surface MUST NOT be a way around the platform's GDPR posture.
  • Recruiter visibility. Successful submissions carry an IsAiAssisted flag and surface as a small "AI-assisted" badge in the recruiter's applications list + detail page. Recruiters can weight the application accordingly; the lifecycle is otherwise identical to a portal apply.

Example โ€” read, update, list applications

curl -H "Authorization: Bearer pk_live_โ€ฆ" \
     https://demo.it-manager-jobs.com/api/v1/me/profile
# 200 OK โ€” full profile bundle

curl -X PUT https://demo.it-manager-jobs.com/api/v1/me/profile \
  -H "Authorization: Bearer pk_live_โ€ฆ" \
  -H "Content-Type: application/json" \
  -d '{
        "headline": "Senior Platform Engineer",
        "expectedSalaryMin": 95000,
        "expectedSalaryMax": 120000,
        "expectedSalaryCurrency": "GBP",
        "careerStatus": "OpenToOffers"
      }'
# 200 OK โ€” returns the patched profile

curl -H "Authorization: Bearer pk_live_โ€ฆ" \
     "https://demo.it-manager-jobs.com/api/v1/me/applications?status=Interviewing&pageSize=10"

curl -X POST https://demo.it-manager-jobs.com/api/v1/me/applications \
  -H "Authorization: Bearer pk_live_โ€ฆ" \
  -H "Content-Type: application/json" \
  -d '{
        "jobId": 1234,
        "confirm": true,
        "coverLetter": "Quick note from the candidate โ€” strong on Kubernetes."
      }'
# 201 Created โ€” body includes { isAiAssisted: true, aiAppliesUsedToday, aiAppliesLimitPerDay }
# 400 if confirm is missing; 409 if you've already applied to this job;
# 429 if you've hit 10 AI applies in the last 24 hours.

Webhooks

Register an HTTPS endpoint and receive signed event POSTs whenever something happens to your jobs or applications. You don't need to poll the API.

Available events

EventWhen it firesPayload includes
application.submitted A candidate submits an application to one of your jobs (any apply method) applicationId, jobPostId, jobTitle, candidateId, appliedAt, applicationType
application.status_changed A recruiter moves an application through the pipeline (New โ†’ Screened โ†’ Interviewed โ†’ ...) applicationId, previousStatus, newStatus, changedAt
job.expired A posting reaches its expiryDate and is auto-expired by the platform jobPostId, expiredAt

Subscribe

Subscriptions are managed at /employer/webhooks. Each subscription gets a fresh HMAC-SHA256 signing secret, returned exactly once on creation.

Verifying signatures

Every delivery includes an X-Webhook-Signature header of the form t={unix-ts},v1={hex-hmac}. Recompute the HMAC on your side and compare in constant time:

// Node.js
const [tPart, v1Part] = req.headers['x-webhook-signature'].split(',');
const t = tPart.split('=')[1];
const v1 = v1Part.split('=')[1];

const signed = `${'$'}{t}.${'$'}{rawBody}`;
const expected = crypto.createHmac('sha256', secret)
                       .update(signed)
                       .digest('hex');

if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1))) {
  return res.status(403).end();
}
if (Math.abs(Date.now()/1000 - Number(t)) > 300) {
  return res.status(400).end(); // older than 5 minutes โ€” replay
}

Headers on every delivery

  • X-Webhook-Signature โ€” t={ts},v1={hmac} as above.
  • X-Webhook-Event โ€” application.submitted etc.
  • X-Webhook-Delivery-Id โ€” stable id for this delivery attempt; useful for de-duplication on retries.

Retries and dead-lettering

Respond with any 2xx within 10 seconds to mark a delivery successful. Anything else (including timeouts) is retried with exponential backoff: 1m โ†’ 5m โ†’ 15m โ†’ 1h โ†’ 6h โ†’ 24h โ†’ 24h โ†’ 24h. After eight failed attempts the delivery is dead-lettered. After ten consecutive failures across any deliveries, the subscription is auto-disabled and the employer admin is emailed.

Idempotency is your responsibility โ€” retries can deliver the same event twice. Key off X-Webhook-Delivery-Id or the entity id in the payload.

Rate limits

Endpoint familyLimit
/api/v1/jobs/... (CRUD)100 req/min per key
/api/v1/applications/... (reads & writes)100 req/min per key
/api/v1/me/... (candidate self)100 req/min per key
/api/v1/candidates/search30 req/min per key
/api/v1/candidates/{id}60 req/min per key
Webhook deliveries (outbound)50 in flight per subscription

Rate-limited requests return 429 Too Many Requests. Back off and retry โ€” credits are not consumed by rate-limited requests.

Errors

Every 4xx response from /api/v1/... follows the same envelope:

{
  "code": "no_credits",
  "message": "Insufficient job posting credits."
}

code is the machine-readable identifier you switch on. message is the human-readable summary, suitable for surfacing in your own UI.

StatusCommon codesWhat to do
400 Bad Requestinvalid_request, invalid_query, invalid_stateFix the request body. Surface the message to your user.
401 Unauthorizedโ€”Key is missing, revoked, or expired. Mint a new one.
402 Payment Requiredno_creditsTop up credits โ€” your contract or balance is exhausted. Calls do not consume credits when they 402.
403 Forbiddenโ€”Key is valid but doesn't carry the scope the endpoint requires. Mint a new key with the right scope.
404 Not Foundnot_foundTenant-scoped โ€” the object exists but doesn't belong to your employer, or doesn't exist at all. The two are deliberately indistinguishable.
429 Too Many Requestsโ€”Back off. Honour Retry-After if present.
5xxโ€”Transient on our side. Retry with backoff; keep idempotency in mind.

Versioning

The public surface is versioned in the URL. v1 is a frozen contract: field renames, removals, status-code changes, or error-envelope tweaks are breaking changes and ship under /api/v2/..., not in v1.

  • New additive changes (extra optional fields on responses, new endpoints) ship within v1 and are non-breaking.
  • v1 stays available for an announced deprecation window when v2 ships โ€” minimum 12 months, more if partner adoption justifies it.
  • Snapshot tests in our CI guard the v1 contract; an accidental shape change breaks the build long before it can ship.

Install the CLI

Drive the platform from your terminal with the jbjobs tool โ€” job posting, applicant pipeline, AI assessments, candidate workflows. Self-contained binary, no .NET runtime install required.

Windows (PowerShell)

iwr -useb https://it-manager-jobs.com/install/jbjobs.ps1 | iex

macOS / Linux

curl -fsSL https://it-manager-jobs.com/install/jbjobs.sh | sh

The installer downloads the latest signed binary from our GitHub releases, verifies its SHA-256 against the published checksums, and drops it on your PATH. Per-user install โ€” no admin / sudo required. Re-running the same command upgrades in place.

After install, point the tool at your tenant:

# Windows (PowerShell)
$env:JOBBOARD_API_BASE = "https://your-tenant.example.com"
$env:JOBBOARD_API_KEY  = "pk_live_โ€ฆ"

# macOS / Linux
export JOBBOARD_API_BASE="https://your-tenant.example.com"
export JOBBOARD_API_KEY="pk_live_โ€ฆ"

jbjobs --help
jbjobs jobs list --status Active

Already have the .NET SDK? You can also install via NuGet: dotnet tool install --global JobBoardCli. The one-line installer above doesn't require .NET โ€” the binary it downloads is self-contained.

OpenAPI & Postman

Two stable, machine-readable definitions for the public surface โ€” pick whichever your toolchain prefers:

We don't yet ship language-specific SDKs; the OpenAPI document is the canonical source. If demand for a packaged client emerges, the most-requested language will land first โ€” let us know via the contact form.

Support

Stuck? Have a partnership question? Need a higher rate limit, an unlimited contract, or a custom event we haven't built yet? Contact us โ€” we read every message and we're happy to walk you through how the API would fit your team's workflow.

Production incidents that affect API availability are tracked at /healthz/ready (returns 200 when the platform is healthy). For ongoing partnerships we'll hand you a dedicated technical contact.