On this page
Build on WebHR
Post jobs and read hiring data in your WebHR workspace from your own systems: an HRIS, a requisition tool, a job board, or a vendor you choose.
https://api.webhr.in/api/v1snake_case, UTC timesCandidates, applications and webhooks come next.
Getting access
- The API is the API Access add-on. Ask your WebHR account manager, or support@webhr.in, to turn it on for your workspace.
- A workspace admin opens Settings → Integrations & Security → API Keys and creates a key: a name, the scopes it needs, and optionally the IP addresses it may be used from and an expiry date.
- WebHR shows the
client_idandclient_secretonce. Keep the secret in your secrets manager; WebHR stores only a fingerprint and can't show it again. If it's lost, rotate the key.
A key acts with the access of the admin who created it, and stops working if that person is no longer a workspace admin. Before an admin leaves, have another admin create replacement keys.
Quick start
1. Get a token
curl -X POST https://api.webhr.in/api/v1/auth/token \
-H "Content-Type: application/json" \
-d "{\"client_id\": \"$WEBHR_CLIENT_ID\", \"client_secret\": \"$WEBHR_CLIENT_SECRET\"}"
import os, requests
token = requests.post("https://api.webhr.in/api/v1/auth/token", json={
"client_id": os.environ["WEBHR_CLIENT_ID"],
"client_secret": os.environ["WEBHR_CLIENT_SECRET"],
}, timeout=10).json()["access_token"]
const res = await fetch("https://api.webhr.in/api/v1/auth/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
client_id: process.env.WEBHR_CLIENT_ID,
client_secret: process.env.WEBHR_CLIENT_SECRET,
}),
});
const { access_token: token } = await res.json();
{
"access_token": "eyJhbGciOi…",
"token_type": "bearer",
"expires_in": 3600,
"scopes": ["recruitment.jobs:read"]
}
2. Call the API
curl "https://api.webhr.in/api/v1/recruitment/jobs?status=published" \
-H "Authorization: Bearer $WEBHR_TOKEN"
jobs = requests.get("https://api.webhr.in/api/v1/recruitment/jobs",
params={"status": "published"},
headers={"Authorization": f"Bearer {token}"}, timeout=10)
jobs.raise_for_status()
for job in jobs.json()["items"]:
print(job["job_code"], job["title"])
const jobs = await fetch("https://api.webhr.in/api/v1/recruitment/jobs?status=published", {
headers: { Authorization: `Bearer ${token}` },
});
const { items } = await jobs.json();
{
"items": [{ "id": 42, "job_code": "JOB-0042", "title": "Welder", "status": "published", … }],
"total": 1,
"page": 1,
"page_size": 25
}
Request builder
Pick an endpoint and fill in its fields to get a command you can run. It's built from the same OpenAPI file as the reference, so it always matches the API.
Nothing is sent from this page, and it never asks for your client secret. Run the command on your
server, with a token from POST /auth/token in WEBHR_TOKEN.
Authentication
- Swap the key for a token at
POST /auth/token. A token lasts one hour; there are no refresh tokens, so ask for a new one when it expires or when a call answers401. - Send the token in the
Authorization: Bearer …header on every call. It is refused anywhere else: in a query string, a cookie or the body. - The API is for server-to-server use. It sends no CORS headers, so a browser page can't call it, and a client secret must never ship in a web page or a mobile app.
- Rotating or revoking a key in WebHR stops its tokens on their next call, not when they expire.
- Your workspace is identified by the key alone. Headers or fields naming another workspace are ignored.
Scopes
Give each key only what it needs. A call outside the key's scopes answers 403; narrowing a key's scopes
in WebHR takes effect on its next call.
| Scope | Allows |
|---|---|
recruitment.jobs:read | List and read jobs; read the hiring stages |
recruitment.jobs:create | Create jobs (always as drafts) |
recruitment.jobs:update | Edit, publish and close jobs |
master_data:read | Read departments, designations, legal entities and business units |
Paging and syncing
Lists take page (from 1) and page_size (default 25, at most 200), and answer
{"items": […], "total": n, "page": 1, "page_size": 25}. Keep asking for the next page while
page × page_size < total.
To keep a copy in sync, pass updated_since (an ISO 8601 time) to GET /recruitment/jobs: it
returns only jobs changed since then, most recently changed first. Store the time each sync started and use it next time.
Errors
Every error has the same shape. Quote the request_id when you contact support; a 422 also lists
each field's problem in errors.
{ "detail": "This key lacks the recruitment.jobs:create scope", "request_id": "5e7250c4-…" }
| Status | Meaning | What to do |
|---|---|---|
| 401 | No token, or it's expired, revoked, rotated or not valid | Get a new token; if that fails, check the key in WebHR |
| 403 | The key lacks the scope, the add-on is off, or the address isn't on the key's allowlist | Check the key's scopes and IP allowlist |
| 404 | Not found. Other workspaces' records answer exactly the same way. | Check the id |
| 409 | Conflicts with the current state, such as publishing a job that is already published | Read the record and decide |
| 422 | Not valid: an unknown or protected field, a value out of range | Fix the request; don't retry it unchanged |
| 429 | Over a rate limit | Wait Retry-After seconds, then retry |
| 5xx | Our fault | Retry with backoff; send us the request_id if it persists |
Rate limits
- 50 calls a minute per key. Every answer carries
X-RateLimit-LimitandX-RateLimit-Remaining; over the limit you get429withRetry-After. POST /auth/token: 30 calls per 10 minutes per IP address. After 10 failed attempts for aclient_idwithin 15 minutes, that client is locked out for the rest of the 15 minutes, even with the right secret.- Reuse a token for its hour; don't get a new one for every call.
Retrying safely
Send an Idempotency-Key header (any unique string up to 100 characters, such as a UUID) on every
POST. If the network drops and you send the same request with the same key within 24 hours, WebHR answers
with the first result instead of acting twice and adds Idempotent-Replayed: true. The same key with a
different body answers 422.
Jobs
POST /recruitment/jobsalways creates a draft. Publish it withPOST /recruitment/jobs/{job_id}/publish; take it off the careers page with/close. A job's status changes only through these two calls.- Fields the API doesn't accept, such as a job's workspace, status, author or dates, are refused with
422rather than ignored, so a mistake in your integration shows up straight away. - Use the names from
GET /department-mastersandGET /designation-mastersso jobs match your workspace's lists.hiring_manager_employee_idmust be an employee of your workspace.
Careers page
Your public careers page needs no key. These two endpoints answer anyone, so they are limited per visitor address.
| Call | Notes |
|---|---|
GET https://api.webhr.in/api/careers/{workspace}/jobs | Your published jobs, as a list. Add
page and page_size to page it; the total is in the X-Total-Count header.
120 calls a minute per address. |
POST https://api.webhr.in/api/careers/{workspace}/jobs/{job_id}/apply | An application from
your site. Send consent: true and the consent_text the applicant agreed to (India's DPDP Act);
consent: false is refused. Add a field named contact_me_by_fax_only, hidden with CSS and left
empty: bots fill it in and are quietly dropped. 10 applications an hour per address. |
Security
- Keep the client secret on your server. Never put it in a web page, a mobile app, a URL or a log.
- Give each integration its own key with the fewest scopes, and an IP allowlist when its addresses are fixed.
- Rotate keys regularly, and at once if a secret may have leaked. Revoke keys you no longer use.
- WebHR records every call a key makes; admins see the last 30 days under API Keys → Calls.
- Found a vulnerability? Please tell security@webhr.in before anyone else.
Versioning
Version 1 lives at /api/v1. Within it we only add endpoints, fields and values, so write
clients that ignore fields they don't know. A change that could break a client gets a new version, /api/v2,
with at least 12 months' notice before v1 changes.
Support
Email support@webhr.in with the request_id of the call in question.
