Build with ITManagerJobs
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
You need an employer account on ITManagerJobs 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.
Create the account
Create an employer account if you don't already have one. Self-serve, no credit card needed to explore.
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.
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.
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.
Pick the scopes
JobPosting covers /api/v1/jobs/...; 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.
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.
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 Unauthorizedimmediately, with no body content. - The
LastUsedAttimestamp 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://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
| Method | Path | Description | Cost |
|---|---|---|---|
| GET | /api/v1/jobs | List your jobs (paginated, filterable by status) | Free |
| GET | /api/v1/jobs/{id} | Single job + full description | Free |
| POST | /api/v1/jobs | Create a draft job | Free |
| PUT | /api/v1/jobs/{id} | Patch any subset of mutable fields | Free |
| POST | /api/v1/jobs/{id}/publish | Draft → 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://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://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"CV Search API Scope: CvSearch
| Method | Path | Description | Cost |
|---|---|---|---|
| POST | /api/v1/candidates/search | Boolean search. Returns masked rows (no contact details). | 1 CV Search credit |
| GET | /api/v1/candidates/{id} | Full profile + email. Free if you've unlocked the candidate within the last 30 days. | 1 CV Download credit (or free) |
Boolean query syntax matches Elasticsearch's query_string: phrase quoting, AND/OR/NOT, parentheses, wildcards. Examples:
"Process Engineer" AND "HVAC"(("High" OR "Low") AND "voltage") AND "Engineers" AND "Dubai"kubernetes AND (aws OR gcp) AND NOT junior
Example
curl -X POST https://it-manager-jobs.com/api/v1/candidates/search \
-H "Authorization: Bearer pk_live_…" \
-H "Content-Type: application/json" \
-d '{
"query": "\"Process Engineer\" AND \"HVAC\"",
"preferredCountryIds": [44],
"activeWithinDays": 90,
"pageNumber": 1,
"pageSize": 20
}'
# 200 OK — items are masked (e.g. "fullName": "P*** E***")
# Each item carries an "isUnlocked" flag.
curl -H "Authorization: Bearer pk_live_…" \
https://it-manager-jobs.com/api/v1/candidates/12345
# 200 OK — full profile + email
# Subsequent calls within 30 days are free (uses the existing unlock).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
| Event | When it fires | Payload 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.submittedetc.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 family | Limit |
|---|---|
/api/v1/jobs/... (CRUD) | 100 req/min per key |
/api/v1/candidates/search | 30 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.
| Status | Common codes | What to do |
|---|---|---|
| 400 Bad Request | invalid_request, invalid_query, invalid_state | Fix the request body. Surface the message to your user. |
| 401 Unauthorized | — | Key is missing, revoked, or expired. Mint a new one. |
| 402 Payment Required | no_credits | Top 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 Found | not_found | Tenant-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.
OpenAPI & SDKs
Fully machine-readable OpenAPI 3 specification:
/swagger/v1/swagger.json
Generate clients in any language using the OpenAPI generator of your choice (openapi-generator, Kiota, NSwag, etc.) or import the spec into Postman for ad-hoc exploration.
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.