API keys
The subscribing owner makes and revokes keys in Settings, API keys. Each key reads only its own company’s records, with the permissions chosen when it is made.
BlueTac shows a key once, when it is made, and keeps only its last four characters and a one-way hash of it, so a key cannot be shown again. If a key is lost or someone else may have seen it, revoke it and make a new one. A revoked key stops working at once and cannot be turned back on.
A company can have 10 active keys and make 20 keys a day.
Pay rates, bill rates, home addresses and tax identification numbers are never available through the API.
Permissions
Each key has one or more of these permissions:
sites:read: Read sites: name, address, time zone and coverage rules. Never bill rates.shifts:read: Read shifts: site, times, status and the assigned employee’s id. Never pay rates.
A request for something the key has no permission for answers 403 insufficient_scope.
Sending a request
Send each request to https://www.bluetac.com/api/v1/… with the key in the Authorization header: Authorization: Bearer btk_…. Version 1 answers GET and HEAD only.
Never put a key in the address: addresses are often logged, so BlueTac refuses a request with a key in its address. Revoke that key and make a new one. Signing in to BlueTac does not open the API; only a key does.
curl "https://www.bluetac.com/api/v1/shifts?status=open&limit=10" \
-H "Authorization: Bearer $BLUETAC_API_CREDENTIAL"Every answer is JSON, with the headers BlueTac-Api-Version: 1 and X-Request-Id.
A script that reads every open shift, a page at a time, and waits when asked to (JavaScript, as a module):
const key = process.env.BLUETAC_API_CREDENTIAL;
const first = 'https://www.bluetac.com/api/v1/shifts?status=open';
let url = first;
for (;;) {
const response = await fetch(url, {headers: {Authorization: 'Bearer ' + key}});
if (response.status === 429) {
const seconds = Number(response.headers.get('Retry-After')) || 1;
await new Promise(done => setTimeout(done, seconds * 1000));
continue;
}
const page = await response.json();
if (!response.ok) throw new Error(page.error.code + ': ' + page.error.message);
for (const item of page.data) console.log(item.start, item.site_id, item.status);
if (!page.has_more) break;
url = first + '&cursor=' + page.next_cursor;
}Endpoints
GET /api/v1/sites: your company’s sites, a page at a time. Needssites:read.GET /api/v1/sites/{id}: one site by its id. Needssites:read.GET /api/v1/shifts: your company’s shifts, a page at a time. Needsshifts:read.GET /api/v1/shifts/{id}: one shift by its id. Needsshifts:read.
To narrow /api/v1/shifts, add any of these to its address:
site_id: only the shifts of this site.status: only shifts with this status: open, pending or assigned.starts_after: only shifts that start at or after this time.starts_before: only shifts that start before this time.
Write a time in ISO 8601 with its offset, such as 2026-11-10T06:00:00Z or 2026-11-09T22:00:00-08:00. In an address, write the + of an offset as %2B.
An id that is not one of your company’s records answers 404 not_found, whether or not it exists elsewhere.
What a record contains
A site has these fields and no others. A fictional one:
{
"object": "site",
"id": "site-harbor-gate-2",
"name": "Harbor Logistics, Gate 2",
"address": "410 Harbor Way, Tacoma, WA 98421",
"timezone": "America/Los_Angeles",
"jurisdiction": "WA",
"postal_code": "98421",
"external_id": "HL-GATE-2",
"coverage": [
{
"id": "cov-night-gate",
"label": "Night gate post",
"days": [1, 2, 3, 4, 5],
"start": "22:00",
"end": "06:00",
"staff": 1,
"break_minutes": 30
}
]
}A shift has these fields and no others. A fictional one:
{
"object": "shift",
"id": "shift-8c41e2",
"site_id": "site-harbor-gate-2",
"coverage_id": "cov-night-gate",
"title": "Night gate post",
"start": "2026-11-10T06:00:00.000Z",
"end": "2026-11-10T14:00:00.000Z",
"break_minutes": 30,
"status": "assigned",
"employee_id": "person-17",
"acknowledged": true,
"required_license": "unarmed",
"required_training": ["First aid"],
"replacement_needed": false
}Lists and pages
A list answers with up to limit items (50 unless you ask for another number, at most 100), in a fixed order. When has_more is true, ask again with cursor set to next_cursor and the same filters, until has_more is false. A cursor works only with the filters it came with.
{
"object": "list",
"url": "/api/v1/shifts",
"data": [
{
"object": "shift",
"id": "shift-8c41e2",
"site_id": "site-harbor-gate-2",
"coverage_id": "cov-night-gate",
"title": "Night gate post",
"start": "2026-11-10T06:00:00.000Z",
"end": "2026-11-10T14:00:00.000Z",
"break_minutes": 30,
"status": "assigned",
"employee_id": "person-17",
"acknowledged": true,
"required_license": "unarmed",
"required_training": ["First aid"],
"replacement_needed": false
}
],
"has_more": true,
"next_cursor": "eyJ2IjoxLCJhZnRlciI6WyIwMDAwMDAiXSwiZmlsdGVycyI6e319"
}Rate limits
- 60 requests a minute for each key.
- 1,000 requests an hour for each company, all its keys together.
- 30 requests with a missing, wrong or revoked key from one address in 10 minutes.
Past a limit, BlueTac answers 429 with a Retry-After header: the seconds to wait. Answers to a valid key carry RateLimit-Limit: the key’s requests a minute.
Errors
An error answers with its HTTP status and a JSON body like this one:
{
"error": {
"type": "permission_error",
"code": "insufficient_scope",
"message": "This API key cannot read shifts. The company owner can make a key with the shifts:read permission.",
"required_scope": "shifts:read",
"request_id": "5f0c2a7e-1b8d-4c39-9a51-3e2d7c6b4f10"
}
}Each error has a type, a code to act on, a message to read and the request’s request_id; some add param or required_scope. The codes:
400 invalid_parameter: a filter, limit or other parameter is unknown, given twice or not valid; param names it.400 invalid_cursor: the cursor is not from this list, or its filters changed. Start again without a cursor.400 key_in_url: the address carries a key. Revoke that key and make a new one.401 invalid_api_key: the key is missing, wrong or revoked.403 insufficient_scope: the key does not have the permission; required_scope names it.403 company_unavailable: the company’s BlueTac access is not active for this key: for example its subscription has ended, or the person who made the key is no longer the subscribing owner.404 route_not_found: there is no such endpoint.404 not_found: your company has no record with this id.405 method_not_allowed: version 1 is read-only: use GET or HEAD.429 rate_limited: past a rate limit: wait the Retry-After seconds.500 internal_error: something went wrong at BlueTac: try again later.503 api_unavailable: the API is not available right now: try again later.
Versions
This is version 1 of the API. BlueTac will give at least 12 months’ notice before retiring an API version, as the Terms of Service say.
Your company’s data
Anyone holding a key can read what it allows until the owner revokes it. Your company is responsible for keeping its keys safe, and for the software it gives them to, as the Terms of Service say.