JobEng API

Connect your careers site, applicant tracking system (ATS) or HR software to JobEng · Version 1.0 · OpenAPI file

The JobEng API lets your own systems read and manage your company's jobs and applications in JobEng. Typical uses: show your open jobs on your own website, send applications from your careers page into JobEng, copy new applicants into your ATS, or get told the moment something changes.

Getting started

  1. In the JobEng app open Business Settings → API & webhooks (owners and admins only).
  2. Tap New key, give it a name (for example "Careers site") and choose what it may do.
  3. Copy the key straight away — it starts with jbk_live_ and is shown once. Keep it on your server, never in a web page or mobile app.
  4. Call the API:
curl https://api.jobeng.ai/api/public/v1/jobs?status=open \
  -H "Authorization: Bearer jbk_live_YOUR_KEY"

Base URL: https://api.jobeng.ai/api/public/v1. Requests and answers are JSON (UTF-8). Dates are ISO 8601; times are UTC.

Authentication and permissions

Send the key in the Authorization header as a bearer token (or in X-JobEng-Key). Every key belongs to one company and can only ever see and change that company's data.

PermissionAllows
jobs:readList and read jobs
jobs:writeCreate, change and close jobs (includes jobs:read)
applications:readList and read applications, including the candidate's contact details and CV link
applications:writeAdd candidates to your jobs and move applications between stages (includes applications:read)

You can have up to 10 active keys. Revoking a key in the app stops it at once. Keys can be given an expiry date. Every request is logged and shown in the app under Usage.

GET /me   →   {"data":{"company":{"id":"9","name":"Your Company Ltd"},"key":{"id":"3","name":"Careers site","scopes":["jobs:read"]},"limits":{"per_minute":120,"per_day":20000}}}

Jobs

EndpointPermissionWhat it does
GET/jobsjobs:readList jobs. status = open, closed, draft or all (default); plus updated_since, limit, cursor
GET/jobs/{id}jobs:readOne job
POST/jobsjobs:writeCreate a job
PATCH/jobs/{id}jobs:writeChange any of the fields below
POST/jobs/{id}/closejobs:writeClose the job (it leaves your public jobs page and the job boards' feed)

Create a job

curl https://api.jobeng.ai/api/public/v1/jobs \
  -H "Authorization: Bearer jbk_live_YOUR_KEY" -H "Content-Type: application/json" \
  -d '{
    "title": "Quantity Surveyor",
    "location": "Leicester",
    "employment_type": "full_time",
    "salary_min": 45000, "salary_max": 55000, "salary_currency": "GBP", "salary_period": "year",
    "description": "We are looking for a Quantity Surveyor to join our commercial team…",
    "required_skills": ["NRM2", "CostX", "NEC4"],
    "closing_date": "2026-12-31",
    "status": "active"
  }'

Fields: title (required), description, short_description, department, location, employment_type, experience_level, salary_min, salary_max, salary_currency (default GBP), salary_period, required_skills, preferred_skills, qualifications, responsibilities, benefits, closing_date, start_date (YYYY-MM-DD), positions, status (active, draft or paused). An active job appears on jobeng.ai/jobs, in Google for Jobs and in the job-board feed, unless you hid it in the app.

A job

{
  "id": "41", "reference": "API-MFR2K8", "title": "Quantity Surveyor", "status": "active", "open": true,
  "description": "…", "department": "Commercial", "location": "Leicester",
  "employment_type": "full_time", "experience_level": "mid",
  "salary": { "min": 45000, "max": 55000, "currency": "GBP", "period": "year" },
  "required_skills": ["NRM2", "CostX", "NEC4"], "closing_date": "2026-12-31",
  "applications_count": 12, "public_url": "https://jobeng.ai/jobs/41",
  "created_at": "2026-10-05T09:12:44.000Z", "updated_at": "2026-10-05T09:12:44.000Z"
}

Applications

EndpointPermissionWhat it does
GET/applicationsapplications:readList applications. Filters: job_id, stage, updated_since; paging: limit, cursor
GET/applications/{id}applications:readOne application
POST/applicationsapplications:writeAdd a candidate to one of your jobs — for example from the form on your own website
PATCH/applications/{id}applications:writeMove to a stage, add a note, tags or a star

Send an application from your website

curl https://api.jobeng.ai/api/public/v1/applications \
  -H "Authorization: Bearer jbk_live_YOUR_KEY" -H "Content-Type: application/json" \
  -d '{
    "job_id": "41",
    "candidate": { "name": "Amira Hassan", "email": "amira@example.com", "phone": "07700 900123",
                   "location": "Nottingham", "linkedin_url": "https://www.linkedin.com/in/…", "skills": ["NRM2"] },
    "cover_letter": "I have six years on NEC4 projects…",
    "resume_url": "https://files.yourcompany.co.uk/cv/amira.pdf",
    "source": "careers page",
    "consent": true
  }'

The same e-mail address can apply to the same job once every 30 days (a repeat answers 409). resume_url must be an https:// link your team can open.

Move an application

curl -X PATCH https://api.jobeng.ai/api/public/v1/applications/318 \
  -H "Authorization: Bearer jbk_live_YOUR_KEY" -H "Content-Type: application/json" \
  -d '{ "stage": "interview", "note": "Phone screen passed — book a site visit" }'

Stages: new, applied, applicants, reviewed, shortlisted, technical, interview, interviewed, offered, offer_sent, hired, rejected, withdrawn, on_hold. Every move is recorded in the application's history with the key's name; a note is added to the recruiter notes.

An application

{
  "id": "318", "reference": "WEB-MFQ8Z1", "job_id": "41", "job_title": "Quantity Surveyor",
  "candidate": { "name": "Amira Hassan", "email": "amira@example.com", "phone": "07700 900123",
                 "location": "Nottingham", "linkedin_url": "…", "current_title": "Assistant QS", "skills": ["NRM2"] },
  "cover_letter": "…", "resume_url": "https://api.jobeng.ai/files/cv/…",
  "stage": "interview", "source": "web", "scores": { "ai_match": 82, "overall": null },
  "tags": [], "starred": false, "applied_at": "2026-10-04T18:20:00.000Z", "updated_at": "2026-10-05T10:02:11.000Z"
}

Lists and paging

Lists return the newest first, 25 at a time (up to 100 with limit). When has_more is true, ask again with cursor set to next_cursor. To keep a copy in step, poll with updated_since set to the time of your last successful sync — or use webhooks.

{ "data": [ … ], "has_more": true, "next_cursor": "296" }

Errors and limits

Errors have the same shape: {"error": {"code": "…", "message": "…"}}.

StatusCodeMeaning
400bad_requestA query parameter is wrong
401missing_key · invalid_key · expired_keyNo key, an unknown or revoked key, or an expired key
403missing_scopeThe key lacks the permission named in required_scope
404not_foundNo such record in your company
409duplicateThe candidate already applied for this job in the last 30 days
422invalidThe body is not valid — fields lists what to fix
429rate_limited · daily_limitSlow down — wait the number of seconds in Retry-After
500server_errorOur fault — try again; if it continues e-mail info@sesysy.com

Each key may make 120 requests a minute and 20,000 a day. Every answer carries X-RateLimit-Limit and X-RateLimit-Remaining.

Webhooks

Instead of asking again and again, let JobEng tell you. Add an https:// address in the app (API & webhooks → Webhooks), choose the events and keep the signing secret (whsec_…). JobEng checks for changes every minute and sends each event as a POST with a JSON body.

EventWhendata
application.createdA new application arrives (app, jobeng.ai/jobs, your website through the API)an application
application.updatedAn application changes — for example its stagean application
job.createdA job is addeda job
job.updatedA job changesa job
job.closedA job is closed or filleda job
POST https://your-server.example.com/jobeng
JobEng-Event: application.created
JobEng-Delivery: 5521
JobEng-Signature: t=1791200000,v1=5f2b…c9
Content-Type: application/json

{ "id": "evt_8c1f…", "type": "application.created", "created_at": "2026-10-05T10:02:11.000Z",
  "company_id": "9", "api_version": "1.0", "data": { …an application… } }

Answer with any 2xx status within 10 seconds. Anything else is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours. A webhook that fails 50 times in a row is paused and your owners are told in the app. Use Send test in the app to check your address; every delivery and its answer is listed there, and a failed one can be sent again.

Check the signature

Compute HMAC-SHA256 of t + "." + raw body with your signing secret and compare it with v1. Reject requests whose t is more than 5 minutes old.

// Node.js (Express) — use the raw body, not the parsed JSON
const crypto = require('crypto');
app.post('/jobeng', express.raw({ type: 'application/json' }), (req, res) => {
  const [t, v1] = req.get('JobEng-Signature').split(',').map((p) => p.split('=')[1]);
  const expected = crypto.createHmac('sha256', process.env.JOBENG_WHSEC).update(`${t}.${req.body}`).digest('hex');
  const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
  if (!fresh || !v1 || v1.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1))) return res.sendStatus(400);
  const event = JSON.parse(req.body);
  // … handle event.type / event.data …
  res.sendStatus(200);
});
# Python (Flask)
import hmac, hashlib, time, os
@app.post("/jobeng")
def jobeng():
    t, v1 = [p.split("=")[1] for p in request.headers["JobEng-Signature"].split(",")]
    expected = hmac.new(os.environ["JOBENG_WHSEC"].encode(), f"{t}.".encode() + request.get_data(), hashlib.sha256).hexdigest()
    if abs(time.time() - int(t)) > 300 or not hmac.compare_digest(expected, v1):
        return "", 400
    event = request.get_json()
    return "", 200

Events can occasionally arrive more than once or out of order — use the event id to ignore repeats and data.updated_at to keep the newest version.

Data protection

Applications contain personal data. Your company is the controller of that data and JobEng processes it for you under our Data Processing Agreement. When you copy applicants into another system you are responsible for it there: keep keys secret, store only what you need, delete it when your retention period ends, and make sure a person — not only software — makes every hiring decision (UK GDPR Article 22). Applicants can ask for a person to review their application at jobeng.ai/jobs/privacy/request.

See also the Terms of Service, Privacy Policy and sub-processors.