Skip to content
Developers/The Crewnyx API

The Crewnyx API

Your company's data, for the other tools you use.

Read your hours, shifts, tasks, receipts and time off from your own code or a tool like Zapier, and have Crewnyx send a signed message to you the moment someone clocks in, files a receipt or finishes a task, from the phones as well as the website.

Last updated October 10, 2026Sparky Venture Labs LLC · Florida, USA

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_request a parameter isn't right; the message says which
  • 401 missing_key / invalid_key no key, or one that's unknown or revoked
  • 402 plan_required the company's plan doesn't include the API
  • 402 subscription_inactive the company's subscription has lapsed
  • 404 not_found there's nothing with that id
  • 429 rate_limited too many requests this minute
  • 503 unavailable Crewnyx 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.

  • status active (the default) or all
  • limit how many to return, 1 to 500 (100 if you leave it out)
  • cursor the 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.

  • status active (the default) or all
  • limit how many to return, 1 to 500 (100 if you leave it out)
  • cursor the 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, to by start time; the last 30 days if you send neither
  • person_id one person's
  • clock payroll or job_site
  • limit how many to return, 1 to 500 (100 if you leave it out)
  • cursor the 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, to by start time; a week ago to 30 days ahead if you send neither
  • person_id only the shifts that person is on
  • limit how many to return, 1 to 500 (100 if you leave it out)
  • cursor the 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.

  • status open, done or all (the default); open leaves out archived tasks
  • from, to by when the task was made
  • limit how many to return, 1 to 500 (100 if you leave it out)
  • cursor the 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, to by when it was filed; the last 30 days if you send neither
  • person_id one person's
  • limit how many to return, 1 to 500 (100 if you leave it out)
  • cursor the next_cursor from the page before, exactly as it came

GET /api/v1/time-off

Time-off requests and their decisions.

  • from, to days; anything that overlaps them
  • person_id one person's
  • status pending, approved, rejected or canceled
  • limit how many to return, 1 to 500 (100 if you leave it out)
  • cursor the 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.

  • type one of the events below (required)
  • limit 1 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_in someone 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_out someone clocked out. A punch finished, with the time worked and any lunch taken off.
  • receipt_submitted a receipt was filed. A receipt, invoice or packing slip was scanned or uploaded.
  • task_created a task was created. A new task, with who it's assigned to.
  • task_completed a task was finished. A task was marked done.
  • shift_assigned someone was put on a shift. A person was added to a scheduled shift, with the whole crew.
  • leave_requested time off was requested. Somebody asked for time off.
  • leave_decided time off was approved or declined. A request was approved or declined, or the office recorded time off that was already approved.
  • checklist_completed a 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.