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:
| What | Example | Used for |
|---|---|---|
| Base address | https://sunrise.loup.care/api/v1 | Every call |
| API key | loup_3f9a1c2b7d_… | Authorization: Bearer <key> on every call |
| Notification signing secret | whsec_… | 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)
| Permission | Allows |
|---|---|
staffRead | GET /staff, GET /staff/:id; staff notifications |
staffWrite | POST /staff, PATCH /staff/:id |
clientsRead | GET /clients, GET /clients/:id; client notifications |
clientsWrite | POST /clients, PATCH /clients/:id |
careNeeds | Adds careNeeds (the care needs summary) to people supported, and lets you set it |
compliance | Adds compliance (can this carer be rostered?) to carers; GET /staff/:id/compliance; compliance notifications |
events | POST /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:
| Code | Blocks rostering |
|---|---|
on_leave | On leave or suspended |
dbs_missing / dbs_expired | No DBS recorded / DBS renewal date has passed |
dbs_decision | The agency decided they cannot work in this role |
rtw_missing / rtw_overdue | Right to work not recorded / re-check overdue |
These are warnings only. Your system decides what to do with them:
| Code | Warns |
|---|---|
training_missing / training_expired | Mandatory training not done / out of date |
references | References outstanding or with issues |
dbs_awaiting | DBS 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
| Call | What it does |
|---|---|
GET /ping | Checks your key. Returns the agency, your connection name, your permissions and the locations you can see: [{ "id": "lei", "name": "Leicester branch" }]. |
GET /staff / GET /clients | All current records you can see. |
GET /staff?updatedSince=2026-10-27T09:00:00Z | Only records changed after that time. |
GET /staff?externalId=ROTA-123 | Find the record linked to your id. |
GET /staff?includeLeft=true | Also return people who have left (status: "left"). |
GET /staff/:id / GET /clients/:id | One record. |
GET /staff/:id/compliance | Just the roster check. |
POST /staff | Add 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 /clients | Add 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/:id | Change only the fields you send (same fields as adding). Sending "externalId": null unlinks. |
POST /events | Report 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:
GET /staffandGET /clients.- Match each person to your records, for example by name and date of birth or email, and check the matches with the agency.
PATCHyourexternalIdonto 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
externalIdis linked to the existing record, and you get200with that record and"matched": true. Use itsidfrom 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
409with"code": "possible_duplicate"and amatcheslist. Show the matches to the agency. If one is right,PATCHit with yourexternalId. If it really is a different person with the same name, send the samePOSTagain with"notDuplicate": true. - Someone who left and is coming back: the same checks against people who have left. Nothing is added. You get
409with"code": "previously_left"and their old record inmatches("status": "left"). Ask the agency to reinstate them in Loup (Archives → Reinstate), so their training and history come back. Theiridstays the same, so thenPATCHyourexternalIdonto 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:
| Event | Sent when |
|---|---|
staff.updated | A carer is added or their details change |
staff.compliance_changed | Something that affects rostering changed: training, DBS decision (needs compliance) |
staff.left | A carer leaves (archived in Loup), with status: "left" |
client.updated | A person supported is added or changed |
client.left | A person supported stops receiving care |
ping | The 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:
- Work out
HMAC-SHA256(secret, "<t>.<raw body>")in hex, and compare it withv1using a timing-safe comparison. - Reject anything where
tis 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
2xxwithin 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-Deliveryto 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
| Code | Meaning |
|---|---|
400 | Something in your request is wrong; error explains |
401 | Key missing, wrong, or the agency disconnected you |
403 | Your connection doesn't have that permission |
404 | Not found, or not at a location you can see |
409 | That 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) |
429 | More than 120 calls a minute; slow down |
503 | The 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.