Overview
The API answers in JSON at https://app.crewnyx.com/api/v1. It reads your company's data, and webhooks send you events as they happen. Each company's keys see that company only.
It's part of the Business plan, and every company on a free trial can use it too. The owner or an admin turns it on by making a key under Settings › Integrations › API and webhooks.
API keys
Make a key in Settings › Integrations › API and webhooks. It starts with cnx_live_ and is shown once, when it's made: Crewnyx keeps only a fingerprint of it, so nobody can show it to you again. Make one key per tool, so revoking one doesn't break the others. A company can have up to 10.
Send it with every request:
curl https://app.crewnyx.com/api/v1/me \
-H "Authorization: Bearer cnx_live_your_key_here"Treat a key like a password. Anyone holding it can read your company's hours, receipts and people. Revoking it stops it at once and removes any webhooks it set up.
Requests and paging
Times are UTC, in ISO 8601 (2026-10-07T13:04:00Z). Days, like a time-off date, are YYYY-MM-DD. Money comes twice: amount_cents as a whole number and amount as a decimal string.
Lists come oldest first, a page at a time. When there's more, the answer carries a next_cursor; pass it back as cursor for the next page. It's null on the last one.
{
"data": [ … ],
"next_cursor": "eyJvIjoxMDB9"
}from and to take a day (2026-10-01, a whole UTC day) or a moment with a zone (2026-10-01T04:00:00Z). To match your company's own midnight, send the moment.
Rate limits
Each key can make 120 requests a minute. Every answer says where you are with X-RateLimit-Limit and X-RateLimit-Remaining. Past the limit you get a 429 with Retry-After: 60.
Errors
A refusal always looks like this, with a sentence a person can act on:
{
"error": {
"code": "invalid_key",
"message": "That API key isn't valid. It may have been revoked; the office can make a new one.",
"docs": "https://app.crewnyx.com/developers"
}
}400 invalid_requesta parameter isn't right; the message says which401 missing_key/invalid_keyno key, or one that's unknown or revoked402 plan_requiredthe company's plan doesn't include the API402 subscription_inactivethe company's subscription has lapsed404 not_foundthere's nothing with that id429 rate_limitedtoo many requests this minute503 unavailableCrewnyx couldn't answer just now; try again in a minute
Codes never change once published. A 503 is never an empty list: if Crewnyx can't read something, it says so rather than answer with nothing.
Endpoints
GET /api/v1/me
Which company the key belongs to. Good for testing a connection.
{
"company": { "id": "…", "name": "Riverside Audio", "timezone": "America/New_York" },
"key": { "name": "Zapier", "prefix": "cnx_live_Ab3dE9" }
}GET /api/v1/people
Your company's people.
statusactive (the default) or alllimithow many to return, 1 to 500 (100 if you leave it out)cursorthe next_cursor from the page before, exactly as it came
{ "data": [{ "id": "…", "name": "Jordan Rivera", "role": "employee", "team": "Install",
"status": "active", "joined_at": "2026-03-02T14:00:00Z" }], "next_cursor": null }GET /api/v1/sites
Your job sites: names and addresses.
statusactive (the default) or alllimithow many to return, 1 to 500 (100 if you leave it out)cursorthe next_cursor from the page before, exactly as it came
GET /api/v1/time-entries
Punches, by when they started. clock is payroll for the clock that's paid, or job_site for time at a job. minutes_worked is after any lunch taken off, and is null while someone is still clocked in. clock_in_method is button, nfc_tag (they tapped the job's tag) or geofence; auto_clocked_out is true when Crewnyx closed a job visit because they'd left the site. Voided punches are left out.
from, toby start time; the last 30 days if you send neitherperson_idone person'sclockpayroll or job_sitelimithow many to return, 1 to 500 (100 if you leave it out)cursorthe next_cursor from the page before, exactly as it came
{
"data": [{
"id": "…",
"clock": "job_site",
"person": { "id": "…", "name": "Jordan Rivera" },
"site": { "id": "…", "name": "Miller residence", "address": "1306 Richmond Ave, Asheville, NC 28801" },
"job_type": "installation",
"clock_in": "2026-10-07T13:14:00Z",
"clock_out": "2026-10-07T19:47:00Z",
"minutes_worked": 363,
"lunch_minutes": 30,
"has_notes": true,
"entered_by_office": false,
"clock_in_method": "nfc_tag",
"auto_clocked_out": false
}],
"next_cursor": null
}GET /api/v1/shifts
Scheduled shifts with their crew, who leads, whether each person confirmed, your own shift fields, each shift's to-dos with who ticked them off, and the checklists that go with it with who filled each one in.
from, toby start time; a week ago to 30 days ahead if you send neitherperson_idonly the shifts that person is onlimithow many to return, 1 to 500 (100 if you leave it out)cursorthe next_cursor from the page before, exactly as it came
GET /api/v1/tasks
Tasks, with who they're assigned to, tags, and who finished them.
statusopen, done or all (the default); open leaves out archived tasksfrom, toby when the task was madelimithow many to return, 1 to 500 (100 if you leave it out)cursorthe next_cursor from the page before, exactly as it came
GET /api/v1/receipts
Receipts, invoices and packing slips, with the amount, payment, category and job.
from, toby when it was filed; the last 30 days if you send neitherperson_idone person'slimithow many to return, 1 to 500 (100 if you leave it out)cursorthe next_cursor from the page before, exactly as it came
GET /api/v1/time-off
Time-off requests and their decisions.
from, todays; anything that overlaps themperson_idone person'sstatuspending, approved, rejected or canceledlimithow many to return, 1 to 500 (100 if you leave it out)cursorthe next_cursor from the page before, exactly as it came
GET /api/v1/events
The newest events of one type, exactly as a webhook carries them: the real ones first, then samples built from your latest records, marked "sample": true.
typeone of the events below (required)limit1 to 25 (3 if you leave it out)
GET, POST /api/v1/webhooks · GET, DELETE /api/v1/webhooks/{id}
Webhooks this key set up. POST one to start receiving events and DELETE it to stop. The answer to a POST carries the webhook's signing secret, once. A key sees and removes only its own; webhooks the office added in Settings aren't listed.
curl https://app.crewnyx.com/api/v1/webhooks \
-H "Authorization: Bearer cnx_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/crewnyx", "events": ["clock_out", "receipt_submitted"] }'
{ "id": "…", "url": "https://example.com/crewnyx", "events": ["clock_out", "receipt_submitted"],
"secret": "whsec_…", "created_at": "2026-10-10T15:00:00Z" }Webhooks
A webhook is an address of yours that Crewnyx sends a POST to the moment something happens. Events are raised by the database itself, so a punch from the iPhone or Android app is sent exactly like one made on the website. Add one in Settings › Integrations › API and webhooks, or let a tool set its own up with a key.
Every delivery looks like this, with the same shape the API answers with under data:
POST https://example.com/crewnyx
Content-Type: application/json
Crewnyx-Event: clock_out
Crewnyx-Delivery: 5d0c…
Crewnyx-Signature: t=1760108820,v1=6f1c…
{
"id": "0b8e…",
"type": "clock_out",
"created_at": "2026-10-07T19:47:00Z",
"company_id": "…",
"data": { …the time entry, as GET /api/v1/time-entries has it… }
}The id is the event's: the same event sent twice (a retry) has the same id, so you can skip one you've already handled. If the thing is gone by the time it's sent, data is null. Answer with any 2xx within 10 seconds; do the slow work afterwards.
Events
clock_insomeone clocked in. A punch started, on the payroll clock or at a job, from any app. Not an entry the office types in by hand.clock_outsomeone clocked out. A punch finished, with the time worked and any lunch taken off.receipt_submitteda receipt was filed. A receipt, invoice or packing slip was scanned or uploaded.task_createda task was created. A new task, with who it's assigned to.task_completeda task was finished. A task was marked done.shift_assignedsomeone was put on a shift. A person was added to a scheduled shift, with the whole crew.leave_requestedtime off was requested. Somebody asked for time off.leave_decidedtime off was approved or declined. A request was approved or declined, or the office recorded time off that was already approved.checklist_completeda checklist was submitted. A checklist or inspection was filled in, with its answers, and the shift it was for when there was one.
A punch the office types in by hand doesn't send clock_in or clock_out: it's a record of the past, not something happening. Read those from /api/v1/time-entries.
Checking signatures
Every delivery is signed with the webhook's secret, so you can tell it came from Crewnyx and wasn't changed. Crewnyx-Signature holds the time it was signed (t, in Unix seconds) and an HMAC-SHA256 of t, a dot, and the raw body (v1, in hex), made with the whole secret as the key. Check it against the body exactly as it arrived, before parsing it, and refuse anything signed more than five minutes ago.
import crypto from "node:crypto";
function fromCrewnyx(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const given = Buffer.from(parts.v1 ?? "", "hex");
return given.length === 32 && crypto.timingSafeEqual(given, Buffer.from(expected, "hex"));
}Lost the secret? Make a new one from the webhook in Settings. The old one stops working at once.
Retries and turning off
When a delivery doesn't get a 2xx, Crewnyx tries again after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours: eight tries over about two days. Every try, and what your server answered, is listed under the webhook in Settings, where anything that didn't arrive can be sent again.
A webhook that has failed for three days with no success in between is turned off, and whoever added it gets an email. So is one that answers 410 Gone, which is how to say "stop sending here." Turn it back on in Settings once it's fixed.
Zapier and similar tools
Zapier, and any tool that subscribes with REST hooks, works without anything extra: test the connection with GET /api/v1/me, subscribe with POST /api/v1/webhooks when a Zap is switched on, unsubscribe with DELETE /api/v1/webhooks/{id} when it's switched off, and fetch sample data with GET /api/v1/events?type=…. A POST also accepts target_url or hookUrl in place of url, and one event in place of a list.
What the API leaves out
Some things stay in Crewnyx, whatever a key can read:
- site gate codes, alarm codes and Wi-Fi passwords
- the reason someone gave for time off, which is often medical
- where a punch was made, and selfies taken at the clock
- receipt photos, and pay rates
Everything the API does read is your company's own, and nothing from any other company.
Getting help
Email support@crewnyx.com with what you sent, what came back, and the Crewnyx-Delivery id for a webhook. Never send us a key or a secret: we'll never need one.