Partner API
Jobs
Post a job to a company you manage, publish it, and move it through draft, live, closed and archived.
Jobs belong to a company your app created. If you have not made one yet, start with Companies.
Lifecycle
| Field | Type | Description |
|---|---|---|
| draft | Not visible | Created but not published. Edit freely. |
| active | Live | On the Tabbio job board, matched to candidates, accepting applications. |
| closed | Off the board | No new applications. Existing candidates stay readable, and it can be published again. |
| archived | Hidden | Out of everyday management. Still readable by id, and no longer editable. |
The transitions are publish (draft or closed to active), close (active to closed)
and archive (draft or closed to archived). Archiving is not a shortcut for
unpublishing: a live job has to be closed first.
Post a job
https://server.tabbio.com/v1/companies/{companyId}/jobsRequires jobs:write. The body is grouped into sections rather
than flat: basics, content, compensation, eligibility, questions,
distribution and confidential.
saveAs decides what happens on create. It defaults to draft; send "live" to
publish in the same call, which means the body has to satisfy the full publish contract
straight away.
curl -X POST https://server.tabbio.com/v1/companies/$COMPANY_ID/jobs \
-H "Authorization: Bearer $TABBIO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: crm-req-4412-ops-manager" \
-d '{
"saveAs": "live",
"basics": {
"title": "Regional Operations Manager",
"department": "Operations",
"country": "United Arab Emirates",
"city": "Dubai",
"workMode": "onsite",
"employment": "full_time",
"seniority": "senior"
},
"content": {
"summary": "Own the regional operations function end to end across the Gulf network.",
"responsibilities": ["Run the daily operation across three hubs"],
"requirements": ["Seven years running regional logistics operations"],
"benefits": ["Health cover"]
},
"compensation": {
"min": 25000,
"max": 35000,
"currency": "AED",
"period": "monthly",
"visible": true
},
"questions": [],
"distribution": "tabbio_and_company"
}'const response = await fetch(
`https://server.tabbio.com/v1/companies/${companyId}/jobs`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TABBIO_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": `crm-req-${requisition.id}`,
},
body: JSON.stringify({
saveAs: "live",
basics: {
title: "Regional Operations Manager",
department: "Operations",
country: "United Arab Emirates",
city: "Dubai",
workMode: "onsite",
employment: "full_time",
seniority: "senior",
},
content: {
summary: "Own the regional operations function end to end across the Gulf network.",
responsibilities: ["Run the daily operation across three hubs"],
requirements: ["Seven years running regional logistics operations"],
benefits: ["Health cover"],
},
compensation: { min: 25000, max: 35000, currency: "AED", period: "monthly", visible: true },
questions: [],
distribution: "tabbio_and_company",
}),
},
);
const { data: job } = await response.json();import os
import requests
response = requests.post(
f"https://server.tabbio.com/v1/companies/{company_id}/jobs",
headers={
"Authorization": f"Bearer {os.environ['TABBIO_API_KEY']}",
"Idempotency-Key": f"crm-req-{requisition_id}",
},
json={
"saveAs": "live",
"basics": {
"title": "Regional Operations Manager",
"department": "Operations",
"country": "United Arab Emirates",
"city": "Dubai",
"workMode": "onsite",
"employment": "full_time",
"seniority": "senior",
},
"content": {
"summary": "Own the regional operations function end to end across the Gulf network.",
"responsibilities": ["Run the daily operation across three hubs"],
"requirements": ["Seven years running regional logistics operations"],
"benefits": ["Health cover"],
},
"compensation": {
"min": 25000,
"max": 35000,
"currency": "AED",
"period": "monthly",
"visible": True,
},
"questions": [],
"distribution": "tabbio_and_company",
},
)
response.raise_for_status()
job = response.json()["data"]basics
| Field | Type | Description |
|---|---|---|
| titlerequired | string | The role. Keep it to the job title people search for, without a location or a requisition number. |
| workModerequired | string | One of |
| employmentrequired | string | One of |
| seniorityrequired | string | One of |
| department | string | Free text. |
| country | string | Required in practice to publish, and to match the job to candidates. |
| city | string | Where the work happens. |
content
| Field | Type | Description |
|---|---|---|
| summaryrequired | string | Markdown, up to 8000 characters. This is what a candidate reads first. |
| responsibilitiesrequired | string[] | One line each. |
| requirementsrequired | string[] | One line each. |
| niceToHave | string[] | |
| benefits | string[] |
compensation
| Field | Type | Description |
|---|---|---|
| currencyrequired | string | ISO 4217, for example AED or SAR. |
| periodrequired | string | One of |
| visiblerequired | boolean | Whether the range is shown publicly. A hidden range is still used for matching. |
| min | number | Greater than zero. |
| max | number | Greater than zero. |
distribution and confidential
distribution is required and decides where the job appears.
| Field | Type | Description |
|---|---|---|
| tabbio_and_company | string | The Tabbio job board and the company career page. |
| company_page_only | string | The career page only, not the board. |
| hidden | string | Neither. Reachable only by direct link. |
confidential takes { enabled, publicLabel } and hides the employer name behind a
label such as "A logistics group in Dubai".
eligibility and questions
eligibility narrows who the job is matched to: candidateLocation,
experienceYears, language, workEligibility, and a workforceProgramme block for
Emiratisation and Saudization. Each is an enum; the reference page lists the values.
questions is up to 30 screening questions. Each has a prompt, a type
(yes_no, multiple_choice, short_answer, long_answer, number), a required
flag, and options for a multiple choice. The answers come back on every application.
Idempotency-Key
Send an Idempotency-Key header, at most 80 characters, and a retry with the same key
returns the job the first attempt created instead of posting a duplicate. Keys are
namespaced per company, so the same key against two companies is two jobs. The header
wins over an idempotencyKey in the body.
Derive it from your own record, so a retry after a network timeout naturally produces the same key.
Retrying without a key double posts
A timed-out request may well have succeeded. Without the header there is no way for the second attempt to know that, and you end up with two identical live postings.
Publish, close, archive
https://server.tabbio.com/v1/jobs/{jobId}/publishhttps://server.tabbio.com/v1/jobs/{jobId}/closehttps://server.tabbio.com/v1/jobs/{jobId}/archiveAll three need jobs:write and take no body.
curl -X POST https://server.tabbio.com/v1/jobs/$JOB_ID/publish \
-H "Authorization: Bearer $TABBIO_API_KEY"These are not idempotent
Publishing a job that is already live answers 400, not 200. So does closing a job that
is not live, and archiving one that is. Check status first, or treat the 400 as the
no-op it describes rather than retrying it.
Publishing requires the publish contract to be satisfied: location, salary and screening rules. A draft that is missing them answers 400 with the field named.
Update a job
https://server.tabbio.com/v1/jobs/{jobId}Requires jobs:write. Send the sections you are changing, in the same shape as the create. A live job stays live and must keep satisfying the publish contract; an archived job cannot be edited at all.
curl -X PATCH https://server.tabbio.com/v1/jobs/$JOB_ID \
-H "Authorization: Bearer $TABBIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"basics": {
"title": "Regional Operations Manager",
"department": "Supply chain",
"country": "United Arab Emirates",
"city": "Dubai",
"workMode": "onsite",
"employment": "full_time",
"seniority": "senior"
}
}'basics is validated as a whole, so send the whole section rather than the one field
you are changing.
List a company's jobs
https://server.tabbio.com/v1/companies/{companyId}/jobsRequires jobs:read. Newest first, page based. Archived jobs are
excluded unless you ask for them with status=archived.
| Field | Type | Description |
|---|---|---|
| status | string | Filter by lifecycle status. One of |
| page | integer | 1 based page number. Defaults to 1. |
| pageSize | integer | Items per page, 1 to 100. Defaults to 25. |
curl "https://server.tabbio.com/v1/companies/$COMPANY_ID/jobs?status=active&pageSize=100" \
-H "Authorization: Bearer $TABBIO_API_KEY"{
"data": [
{
"id": "job_6p1q8r3s",
"companyId": "cmp_4k8m2n6p",
"title": "Regional Operations Manager",
"status": "active",
"workMode": "onsite",
"employment": "full_time",
"seniority": "senior",
"city": "Dubai",
"country": "United Arab Emirates",
"salary": { "min": 25000, "max": 35000, "currency": "AED", "period": "monthly" },
"salaryVisible": true,
"distribution": "tabbio_and_company",
"screeningQuestions": [],
"applicationCount": 0,
"postedAt": "2026-09-01T10:30:00.000Z",
"createdAt": "2026-09-01T10:22:00.000Z",
"updatedAt": "2026-09-01T10:30:00.000Z"
}
],
"error": null,
"meta": { "page": 1, "pageSize": 100, "total": 1, "totalPages": 1 }
}postedBy and the stored idempotency key are never exposed.
Read one job
https://server.tabbio.com/v1/jobs/{jobId}Requires jobs:read. Works in every state, including archived.
Writing a description that performs
content.summary is markdown and it is what a candidate reads before deciding to
apply. Three things move the numbers more than anything else:
- A visible salary range. Set
compensation.visibleto true. Postings without one are skipped in a market where most have it. - The work mode, stated plainly. "Onsite, Dubai" beats "flexible working".
- A short requirements list. Ten bullets of nice-to-haves reads as a wish list and
filters out the people who would have been fine. Put those in
niceToHave.