work.wrk

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.

Safety rule that applies to every write this API can do: a job created through this API always comes in as a 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.

Header
Authorization: Bearer ipk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Missing or invalid key → 401. Revoked key → 401 (same response — it doesn't distinguish "never existed" from "revoked").

Base URL & rate limit

Base URL
https://<your-inprn-api-host>/api/v1/external

300 requests per 15 minutes, per key. Beyond that you'll get a 429.

GET/clients

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.

FieldRequiredNotes
searchoptionalCase-insensitive, matches anywhere in the client name. Omit to list all (capped at 100).
200 · application/json
{
  "success": true,
  "clients": [
    { "id": "66f1a2b3c4d5e6f7a8b9c0d1", "name": "Acme Security Ltd" }
  ]
}
GET/sites

A client's saved locations, if they have any set up in INPRN.

FieldRequiredNotes
clientIdrequiredA real, active client id from /clients.
200 · application/json
{
  "success": true,
  "sites": [
    { "id": "66f1a2b3c4d5e6f7a8b9c0d2", "name": "Warehouse — Bristol" }
  ]
}
GET/schedule

Read jobs — this is the calendar/schedule read side. Capped at 500 jobs per call; page with dateFrom/dateTo if you need more.

FieldRequiredNotes
dateFromoptionalYYYY-MM-DD, inclusive.
dateTooptionalYYYY-MM-DD, inclusive.
statusoptionalOne of draft, published, completed, cancelled.
clientIdoptionalFilter to one client.
200 · application/json
{
  "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.

POST/jobs

Request a new shift. Always creates a draft — see the safety note above.

Request body
{
  "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"
}
FieldRequiredNotes
clientIdrequiredA real, active client id from /clients.
siteIdoptionalA real, active site id belonging to that client, from /sites. * see below
locationoptionalA free-text one-off address, if not using a saved site. * see below
titlerequiredShift name/title.
descriptionoptionalFalls back to title if omitted.
daterequiredYYYY-MM-DD.
startTime / endTimerequiredHH:mm, 24-hour. End time at or before start time is treated as overnight.
requiredWorkersoptionalDefaults to 1. Max 200.
addressoptionalExtra address detail, only used alongside location (ignored if siteId is set).
notesoptionalShown to the manager reviewing the draft.
externalReferenceoptionalYour own booking/order id — echoed back on every response.

* Provide either siteId or location — one of the two is required.

200 · application/json
{
  "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>" }.

StatusMessageWhen
401Missing or invalid API key.No Authorization header, a malformed key, or a revoked/unknown one.
400A valid clientId is required.clientId missing or not a valid id shape.
400Client not found, inactive, or doesn't belong to this company.clientId doesn't resolve to one of your active clients.
400Site not found, inactive, or doesn't belong to this client.siteId doesn't belong to the given clientId, or is inactive.
400Provide either a siteId or a location for a one-off address.Neither was sent.
400startTime must be HH:mm / endTime must be HH:mmWrong 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.

inprn-client.js
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

error handling
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. 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. 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. 3

    Create the draft.

    When a booking comes in on your site, POST /jobs with your own externalReference for reconciliation.

  4. 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.

Reads and writes are scoped to your company automatically by the API key — you never send a company id yourself. Questions? Contact us.