Developer docs
External API
Connect your own systems — a booking website, a Zapier flow, an internal tool — to your INPRN schedule. Authenticated by an API key, scoped to your company automatically.
draft. It's never published, never assigned a worker, and never visible in a worker's app until a manager reviews and publishes it inside INPRN.Getting a key
In the INPRN app: Settings → API Keys → New key. Only the company owner can create or revoke keys. The raw key is shown exactly once, right after creation — copy it somewhere safe immediately. INPRN only ever stores a hash of it, so if you lose it, the only fix is to revoke it and create a new one.
Authentication
Every request needs the key as a bearer token.
Authorization: Bearer ipk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxMissing or invalid key → 401. Revoked key → 401 (same response — it doesn't distinguish "never existed" from "revoked").
Base URL & rate limit
https://<your-inprn-api-host>/api/v1/external300 requests per 15 minutes, per key. Beyond that you'll get a 429.
Look up your clients by name so you can resolve a name you already know to the id the rest of this API needs. Only active clients are returned.
| Field | Required | Notes |
|---|---|---|
| search | optional | Case-insensitive, matches anywhere in the client name. Omit to list all (capped at 100). |
{
"success": true,
"clients": [
{ "id": "66f1a2b3c4d5e6f7a8b9c0d1", "name": "Acme Security Ltd" }
]
}A client's saved locations, if they have any set up in INPRN.
| Field | Required | Notes |
|---|---|---|
| clientId | required | A real, active client id from /clients. |
{
"success": true,
"sites": [
{ "id": "66f1a2b3c4d5e6f7a8b9c0d2", "name": "Warehouse — Bristol" }
]
}Read jobs — this is the calendar/schedule read side. Capped at 500 jobs per call; page with dateFrom/dateTo if you need more.
| Field | Required | Notes |
|---|---|---|
| dateFrom | optional | YYYY-MM-DD, inclusive. |
| dateTo | optional | YYYY-MM-DD, inclusive. |
| status | optional | One of draft, published, completed, cancelled. |
| clientId | optional | Filter to one client. |
{
"success": true,
"jobs": [
{
"id": "66f2b3c4d5e6f7a8b9c0d1e2",
"title": "Night Shift — Warehouse",
"date": "2026-10-15",
"startTime": "20:00",
"endTime": "06:00",
"status": "published",
"client": "Acme Security Ltd",
"site": "Warehouse — Bristol",
"requiredWorkers": 3,
"assignedWorkers": 1,
"externalReference": null
}
]
}externalReference echoes back whatever you sent when creating the job via this API — null for jobs created inside INPRN itself. This endpoint never returns worker names, contact details, or pay rates.
Request a new shift. Always creates a draft — see the safety note above.
{
"clientId": "66f1a2b3c4d5e6f7a8b9c0d1",
"siteId": "66f1a2b3c4d5e6f7a8b9c0d2",
"title": "Night Shift — Warehouse",
"description": "Security cover for the new stock delivery.",
"date": "2026-10-15",
"startTime": "20:00",
"endTime": "06:00",
"requiredWorkers": 3,
"notes": "Gate code changes weekly — check with site manager.",
"externalReference": "BOOKING-4471"
}| Field | Required | Notes |
|---|---|---|
| clientId | required | A real, active client id from /clients. |
| siteId | optional | A real, active site id belonging to that client, from /sites. * see below |
| location | optional | A free-text one-off address, if not using a saved site. * see below |
| title | required | Shift name/title. |
| description | optional | Falls back to title if omitted. |
| date | required | YYYY-MM-DD. |
| startTime / endTime | required | HH:mm, 24-hour. End time at or before start time is treated as overnight. |
| requiredWorkers | optional | Defaults to 1. Max 200. |
| address | optional | Extra address detail, only used alongside location (ignored if siteId is set). |
| notes | optional | Shown to the manager reviewing the draft. |
| externalReference | optional | Your own booking/order id — echoed back on every response. |
* Provide either siteId or location — one of the two is required.
{
"success": true,
"job": {
"id": "66f2b3c4d5e6f7a8b9c0d1e2",
"title": "Night Shift — Warehouse",
"date": "2026-10-15",
"startTime": "20:00",
"endTime": "06:00",
"status": "draft",
"externalReference": "BOOKING-4471"
}
}status is always "draft" in this response — that's not a bug, it's the whole point. Poll GET /schedule once you need to know a manager has published it.
Error reference
Every failure has the same shape: { "msg": "<human-readable message>" }.
| Status | Message | When |
|---|---|---|
| 401 | Missing or invalid API key. | No Authorization header, a malformed key, or a revoked/unknown one. |
| 400 | A valid clientId is required. | clientId missing or not a valid id shape. |
| 400 | Client not found, inactive, or doesn't belong to this company. | clientId doesn't resolve to one of your active clients. |
| 400 | Site not found, inactive, or doesn't belong to this client. | siteId doesn't belong to the given clientId, or is inactive. |
| 400 | Provide either a siteId or a location for a one-off address. | Neither was sent. |
| 400 | startTime must be HH:mm / endTime must be HH:mm | Wrong time format. |
| 429 | (rate limit response) | More than 300 requests in 15 minutes on this key. |
Example client — Node.js / axios
The key only ever needs to go in the Authorization header — set it once on an axios instance and every call inherits it.
import axios from "axios";
const inprn = axios.create({
baseURL: "https://<your-inprn-api-host>/api/v1/external",
headers: {
Authorization: `Bearer ${process.env.INPRN_API_KEY}`,
},
});
// GET /clients?search=
export async function findClient(name) {
const { data } = await inprn.get("/clients", { params: { search: name } });
return data.clients[0] ?? null;
}
// GET /sites?clientId=
export async function getSitesForClient(clientId) {
const { data } = await inprn.get("/sites", { params: { clientId } });
return data.sites;
}
// GET /schedule?dateFrom=&dateTo=
export async function getSchedule(dateFrom, dateTo) {
const { data } = await inprn.get("/schedule", { params: { dateFrom, dateTo } });
return data.jobs;
}
// POST /jobs — always comes back as status: "draft"
export async function requestBooking(booking) {
const { data } = await inprn.post("/jobs", {
clientId: booking.clientId,
siteId: booking.siteId,
title: booking.title,
date: booking.date,
startTime: booking.startTime,
endTime: booking.endTime,
requiredWorkers: booking.requiredWorkers ?? 1,
notes: booking.notes,
externalReference: booking.orderId,
});
return data.job;
}Handling errors
try {
const job = await requestBooking(booking);
} catch (err) {
if (axios.isAxiosError(err)) {
console.error(err.response?.status, err.response?.data?.msg);
// e.g. 400 "Client not found, inactive, or doesn't belong to this company."
}
throw err;
}A typical integration flow
- 1
Resolve your client id.
Call GET /clients?search=<name> once at setup time — or hardcode it if your integration only ever books for one client.
- 2
Optionally list saved sites.
GET /sites?clientId=... if you want to let the booker pick a saved site rather than typing an address.
- 3
Create the draft.
When a booking comes in on your site, POST /jobs with your own externalReference for reconciliation.
- 4
Wait for publish.
A manager reviews and publishes the draft inside INPRN. Poll GET /schedule and match on externalReference if you want to reflect status back to your own users.