← Integrations

Loup integration API (v1)

For developers connecting a rota or scheduling system to an agency's Loup system.

For developers connecting another system (for example a rota or scheduling system) to an agency's Loup system.

Each agency has its own Loup system at its own address (for example https://sunrise.loup.care). An agency manager creates a connection on their Integrations page and gives you three things privately:

WhatExampleUsed for
Base addresshttps://sunrise.loup.care/api/v1Every call
API keyloup_3f9a1c2b7d_…Authorization: Bearer <key> on every call
Notification signing secretwhsec_…Checking that notifications really came from Loup (only if they gave you a notification address)

The agency chooses what your connection may do, and may limit it to some of their locations. Calls outside that are refused with 403. Records at other locations simply don't exist for you (404). The agency can disconnect you at any time; the key then returns 401. Every call is recorded in the agency's activity log.

Everything is JSON. Dates are YYYY-MM-DD; times of change are ISO 8601. Keep the key secret and on your server; never put it in a web page or app.

Permissions (chosen by the agency)

PermissionAllows
staffReadGET /staff, GET /staff/:id; staff notifications
staffWritePOST /staff, PATCH /staff/:id
clientsReadGET /clients, GET /clients/:id; client notifications
clientsWritePOST /clients, PATCH /clients/:id
careNeedsAdds careNeeds (the care needs summary) to people supported, and lets you set it
complianceAdds compliance (can this carer be rostered?) to carers; GET /staff/:id/compliance; compliance notifications
eventsPOST /events (missed / late visits)

Never shared, whatever the permissions: medical notes, next of kin and GP details, DBS certificate details and decisions, references, documents, audits and training records. (You get a yes/no and reasons for rostering instead.)

Records

Carer / staff member

{
  "id": "st1001",                 // Loup's id — keep it
  "externalId": "ROTA-123",       // your id for them, if linked (see below)
  "name": "Amara Okafor", "role": "Senior Carer",
  "email": "…", "phone": "…", "address": "…",
  "startDate": "2025-08-23",
  "status": "active",             // active | on_leave | left
  "locationIds": ["lei"],          // which of the agency's locations (see /ping)
  "updatedAt": "2026-10-27T09:12:00.000Z",
  "compliance": {                 // only with the "compliance" permission
    "safeToRoster": false,
    "issues": [ { "severity": "block", "code": "dbs_expired", "message": "DBS check has expired" },
                { "severity": "warn",  "code": "training_expired", "message": "Fire safety has expired" } ]
  }
}

safeToRoster is false if anything is a block:

CodeBlocks rostering
on_leaveOn leave or suspended
dbs_missing / dbs_expiredNo DBS recorded / DBS renewal date has passed
dbs_decisionThe agency decided they cannot work in this role
rtw_missing / rtw_overdueRight to work not recorded / re-check overdue

These are warnings only. Your system decides what to do with them:

CodeWarns
training_missing / training_expiredMandatory training not done / out of date
referencesReferences outstanding or with issues
dbs_awaitingDBS result awaited

Person supported

{ "id": "cl2001", "externalId": "C-9", "name": "Margaret Ellison", "phone": "…", "address": "…", "dob": "1938-04-02",
  "startDate": "2024-11-10", "status": "active", "locationIds": ["lei"], "carerIds": ["st1001", "st1004"],
  "careNeeds": "…",               // only with the "careNeeds" permission
  "updatedAt": "…" }

carerIds are the carers the agency has linked to this person, which is useful for continuity of care.

Calls

CallWhat it does
GET /pingChecks your key. Returns the agency, your connection name, your permissions and the locations you can see: [{ "id": "lei", "name": "Leicester branch" }].
GET /staff / GET /clientsAll current records you can see.
GET /staff?updatedSince=2026-10-27T09:00:00ZOnly records changed after that time.
GET /staff?externalId=ROTA-123Find the record linked to your id.
GET /staff?includeLeft=trueAlso return people who have left (status: "left").
GET /staff/:id / GET /clients/:idOne record.
GET /staff/:id/complianceJust the roster check.
POST /staffAdd a carer. Body: name (required), role, email, phone, address, startDate, locationIds, externalId. Returns 201 and the new record, or 200 with "matched": true if they were already in Loup (see No duplicates).
POST /clientsAdd a person supported. Body: name (required), phone, address, dob, startDate, locationIds, externalId, and careNeeds with that permission. Same duplicate check as staff.
PATCH /staff/:id / PATCH /clients/:idChange only the fields you send (same fields as adding). Sending "externalId": null unlinks.
POST /eventsReport a missed or late visit (see below).

The locationIds field:

  • It is required when the agency has more than one location.
  • Use ids from /ping.
  • When you change it, any location your connection can't see is kept as it was.

Everything else about a person (checks, training, documents, leaving) is managed by the agency in Loup. A carer you add can't be rostered until the agency records their DBS and right to work.

Linking your records to ours: externalId

Send your own id as externalId when you add someone, or PATCH it onto an existing Loup record. After that, every record and notification includes it, and GET /staff?externalId=… finds it. Each externalId can only be linked to one record (409 otherwise).

First connection to an agency that already has people in Loup:

  1. GET /staff and GET /clients.
  2. Match each person to your records, for example by name and date of birth or email, and check the matches with the agency.
  3. PATCH your externalId onto each one.

No duplicates

The same carer or person supported is often typed into both systems. Loup makes sure they stay one record, linked to your record, instead of two.

When you add someone (POST), Loup first compares them with the people it already has. People already linked to another of your records are left out, because they are someone else in your system.

  • Definitely the same person: the same name (ignoring capitals, spaces and punctuation) plus the same email or phone number for a carer, or the same date of birth or phone number for a person supported. Nothing is added. Your externalId is linked to the existing record, and you get 200 with that record and "matched": true. Use its id from now on.
  • Possibly the same person: the same name only. Also a carer with the same email, or a person supported with the same date of birth and first name (for example, after a change of surname). Nothing is added. You get 409 with "code": "possible_duplicate" and a matches list. Show the matches to the agency. If one is right, PATCH it with your externalId. If it really is a different person with the same name, send the same POST again with "notDuplicate": true.
  • Someone who left and is coming back: the same checks against people who have left. Nothing is added. You get 409 with "code": "previously_left" and their old record in matches ("status": "left"). Ask the agency to reinstate them in Loup (Archives → Reinstate), so their training and history come back. Their id stays the same, so then PATCH your externalId onto it. If it's a different person, resend with "notDuplicate": true.
  • Nobody similar: added as normal (201).

Send email, phone and date of birth when you have them; that lets Loup match people without asking.

When the agency adds someone in Loup first, you get a staff.updated or client.updated notification with "externalId": null. Before creating them in your system, look for the same person in yours. If you find them, PATCH your externalId onto the Loup record rather than creating a new one.

The agency also sees a warning in Loup if they add someone with the same name as a carer or person supported already there. If that name belongs to someone who left, Loup offers to reinstate them instead.

Events: missed and late visits

POST /events
{ "type": "visit.missed",            // or "visit.late"
  "clientId": "cl2001",              // or "clientExternalId": "C-9"
  "staffId": "st1001",               // optional, or "staffExternalId"
  "date": "2026-10-26", "time": "08:00",
  "minutesLate": 45,                 // for visit.late
  "note": "Carer's car broke down",
  "eventId": "your-unique-id" }      // recommended: sending the same eventId again is ignored

Each event creates an open incident for the agency to review. It is labelled with your system's name and linked to the person, the carer and their location. Returns 201 and { "incidentId": "…" }; a repeat returns { "duplicate": true }.

Notifications from Loup (webhooks)

If the agency gave Loup your notification address, Loup sends a POST within seconds whenever something changes in Loup, except changes your own connection made:

EventSent when
staff.updatedA carer is added or their details change
staff.compliance_changedSomething that affects rostering changed: training, DBS decision (needs compliance)
staff.leftA carer leaves (archived in Loup), with status: "left"
client.updatedA person supported is added or changed
client.leftA person supported stops receiving care
pingThe agency pressed Send test notification
POST https://your-system.example.com/loup-webhook
Content-Type: application/json
X-Loup-Event: staff.updated
X-Loup-Delivery: dl_8c1f…              (unique per notification)
X-Loup-Signature: t=1793030000,v1=5d41402abc4b2a76…

{ "id": "dl_8c1f…", "event": "staff.updated", "createdAt": "…", "agency": "Sunrise Care", "data": { …the record, as above… } }

Check every notification is genuine:

  1. Work out HMAC-SHA256(secret, "<t>.<raw body>") in hex, and compare it with v1 using a timing-safe comparison.
  2. Reject anything where t is more than 5 minutes old.
const crypto = require('crypto');
function isFromLoup(secret, rawBody, header) {
  const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header || ''); if (!m) return false;
  if (Math.abs(Date.now() / 1000 - Number(m[1])) > 300) return false;
  const want = crypto.createHmac('sha256', secret).update(`${m[1]}.${rawBody}`).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(want), Buffer.from(m[2]));
}

Replying and retries:

  • Reply with any 2xx within 8 seconds. Do slow work afterwards.
  • If you don't reply, Loup tries again after about 30 seconds, 5 minutes and 30 minutes.
  • Use X-Loup-Delivery to ignore repeats.

Notifications can still be missed (for example if your system was down for a long time). Keep in step by also calling GET /staff and GET /clients regularly:

  • every few minutes with updatedSince
  • once a day in full, which also refreshes everyone's compliance

Errors and limits

CodeMeaning
400Something in your request is wrong; error explains
401Key missing, wrong, or the agency disconnected you
403Your connection doesn't have that permission
404Not found, or not at a location you can see
409That externalId is already linked, or "code": "possible_duplicate": this person may already be in Loup, or "code": "previously_left": they left and can be reinstated (see No duplicates)
429More than 120 calls a minute; slow down
503The agency's Loup needs a database update; try later

Errors look like { "error": "explanation" }.

Building a connection? Get in touch. We're happy to help you test against a demo system.