Partner API
Companies
Create company pages your app manages, keep the create idempotent with your own identifiers, and page through the list.
A company on Tabbio is a public career page with a name, a logo, a description and its jobs. Jobs belong to companies, so this is the first call in a partner integration.
Everything here needs an API key. See API keys if you do not have one yet.
Create a company
https://server.tabbio.com/v1/companiesRequires companies:write.
curl -X POST https://server.tabbio.com/v1/companies \
-H "Authorization: Bearer $TABBIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Northwind Logistics",
"industry": "Logistics",
"employeeCount": "51-200",
"location": "Dubai, United Arab Emirates",
"description": "Northwind Logistics moves temperature controlled freight across the Gulf, with a fleet of 120 vehicles and warehouses in Dubai and Riyadh.",
"website": "https://northwind.example",
"externalId": "crm-4412"
}'const response = await fetch("https://server.tabbio.com/v1/companies", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TABBIO_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Northwind Logistics",
industry: "Logistics",
employeeCount: "51-200",
location: "Dubai, United Arab Emirates",
description:
"Northwind Logistics moves temperature controlled freight across the Gulf, with a fleet of 120 vehicles and warehouses in Dubai and Riyadh.",
website: "https://northwind.example",
externalId: "crm-4412",
}),
});
const { data: company } = await response.json();
// 201 on the first call, 200 when this externalId already made one.
const created = response.status === 201;import os
import requests
response = requests.post(
"https://server.tabbio.com/v1/companies",
headers={"Authorization": f"Bearer {os.environ['TABBIO_API_KEY']}"},
json={
"name": "Northwind Logistics",
"industry": "Logistics",
"employeeCount": "51-200",
"location": "Dubai, United Arab Emirates",
"description": "Northwind Logistics moves temperature controlled freight across the Gulf, with a fleet of 120 vehicles and warehouses in Dubai and Riyadh.",
"website": "https://northwind.example",
"externalId": "crm-4412",
},
)
response.raise_for_status()
company = response.json()["data"]| Field | Type | Description |
|---|---|---|
| namerequired | string | 2 to 120 characters. Shown on the career page and on every job. |
| industryrequired | string | Free text, up to 80 characters, for example Logistics or Financial services. |
| employeeCountrequired | string | A band rather than a number, for example 51-200. |
| locationrequired | string | City and country as one line. |
| descriptionrequired | string | At least 50 characters, at most 4000. It appears on the public page, so a one-line stub is rejected. |
| slug | string | The career-page URL segment: lowercase letters, digits and hyphens, 3 to 48 characters. Derived from the name when omitted, with a suffix if it is taken. |
| website | string | Absolute URL, or an empty string to clear it. |
| logo | string | Public URL of a square image. Tabbio downloads it and stores its own copy, so a link that later expires is fine. |
| externalId | string | Your own identifier for this company. Makes creates idempotent. |
{
"data": {
"id": "cmp_4k8m2n6p",
"name": "Northwind Logistics",
"slug": "northwind-logistics",
"tagline": null,
"description": "Northwind Logistics moves temperature controlled freight across the Gulf, with a fleet of 120 vehicles and warehouses in Dubai and Riyadh.",
"location": "Dubai, United Arab Emirates",
"website": "https://northwind.example",
"industry": "Logistics",
"employeeCount": "51-200",
"founded": null,
"logo": null,
"banner": null,
"published": true,
"verified": false,
"externalId": "crm-4412",
"createdAt": "2026-09-01T08:00:00.000Z",
"updatedAt": "2026-09-01T08:00:00.000Z"
},
"error": null,
"meta": null
}description has a floor
Anything under 50 characters is refused with PUBLIC_API_VALIDATION_ERROR. It is the
single most common failure on this endpoint, and it exists because the value is the
public career page's only prose.
Idempotency with externalId
externalId is your own identifier: the primary key in your CRM, the account number,
whatever you already have. Send it and the create becomes idempotent.
- First call with a given
externalIdcreates the company and returns 201. - Any repeat returns the existing company unchanged, with 200.
That means a retry after a timeout is safe, and a sync job can call create for every company on every run without producing duplicates.
// Safe to run repeatedly. The first pass creates, later passes read back.
for (const account of await crm.listAccounts()) {
const response = await fetch("https://server.tabbio.com/v1/companies", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TABBIO_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ ...toTabbioCompany(account), externalId: account.id }),
});
const { data: company } = await response.json();
await crm.saveTabbioId(account.id, company.id);
}A repeat does not update
Sending different fields with an externalId you have already used returns the
company as it is. It does not merge your changes. Use PATCH for that.
externalId is unique per app, so two different apps can both use crm-4412 without
colliding.
Read a company
https://server.tabbio.com/v1/companies/{companyId}Requires companies:read.
curl https://server.tabbio.com/v1/companies/$COMPANY_ID \
-H "Authorization: Bearer $TABBIO_API_KEY"Update a company
https://server.tabbio.com/v1/companies/{companyId}Requires companies:write. Only the fields you send change.
curl -X PATCH https://server.tabbio.com/v1/companies/$COMPANY_ID \
-H "Authorization: Bearer $TABBIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "tagline": "Cold chain across the Gulf", "employeeCount": "201-500" }'name, tagline, description, location, website, industry, employeeCount,
logo and socials are updatable.
slug is not, and neither is founded. The career-page URL is a public handle that
other Tabbio surfaces link to, so changing it would break every published job URL and
every link anyone has shared.
List companies
https://server.tabbio.com/v1/companiesRequires companies:read. Newest first. Only companies your app created appear; companies made by another app, or by an employer inside the Tabbio app, are never returned.
| Field | Type | Description |
|---|---|---|
| page | integer | 1 based page number. Defaults to 1. |
| pageSize | integer | Items per page, 1 to 100. Defaults to 25. |
{
"data": [{ "id": "cmp_4k8m2n6p", "name": "Northwind Logistics", "slug": "northwind-logistics" }],
"error": null,
"meta": { "page": 1, "pageSize": 25, "total": 63, "totalPages": 3 }
}async function listAllCompanies() {
const all = [];
let page = 1;
let totalPages = 1;
do {
const url = new URL("https://server.tabbio.com/v1/companies");
url.searchParams.set("page", String(page));
url.searchParams.set("pageSize", "100");
const response = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.TABBIO_API_KEY}` },
});
const { data, meta } = await response.json();
all.push(...data);
totalPages = meta.totalPages;
page += 1;
} while (page <= totalPages);
return all;
}Ownership
A company created through the API is owned by the Tabbio user who owns the app, and is linked to the app. Two consequences worth knowing:
- Your app can read and change it. Nobody else's app can, and reading one you do not
manage answers 403 with
COMPANY_NOT_MANAGED_BY_APP. - It shows up in that owner's employer surfaces inside Tabbio, so a human can look at what the integration created.
There is no way to hand a company over to another app or to un-manage one. Deleting the app removes the link.