CrewLink API

CrewLink API

Schedule crews around the drive.

CrewLink schedules field-service work. Its availability knows every tech's booked jobs, their calendar, their skills and the drive between jobs at that time of day, so a time it offers is a time someone can actually make. The API is what the CrewLink booking page, office app and field app use; you can use all of it to connect your website, CRM or field tools.

Interactive reference: every endpoint, generated from the live schemas, is at /docs (the raw OpenAPI 3 document is at /v1/openapi.json).

These guides online: /docs/guides, public, with search and a page per guide (/docs/guides/<file name without .md>, this page is the index).

#Guides

Guide For
Booking page Taking bookings on your website: services, open times, holds, bookings, deposits, out-of-area requests, and the homeowner's links
Office Running the schedule from your CRM or back office: jobs and the intake queue, team, branches and services, time off, live board, emergencies, client companies, roles and invitations, technician pay, invoices, reports, Wins, your CrewLink plan
Field app Tech-facing tools: the day, job stages, check-in, location, photos, parts, calls, and taps queued offline
CrewConnect Sending work to and receiving work from partner companies and independent contractors
Messaging Messages between your people: the company channel, job threads, office lines, direct messages and CrewConnect partner channels, with who-can-message-whom rules
Webhooks Signed events for bookings, jobs, payments, messages and CrewConnect, with verification code
Platforms Building a product on CrewLink for the companies that grant you access (the integration guide): platform keys and scopes, the booking flow end to end, external ids, job files, techs' hours and bases, one webhook subscription, the sandbox
Sign-in Email-code and passkey sign-in, and sessions, for apps used by people
Admin CrewLink staff only: super admins, every company, audited act-as owner sessions, platforms and grants, the audit log
Sign-up Self-serve sign-up: the plans and rates, a new company and its owner, the card on file (Stripe Checkout, 30-day trial)
Changelog What changed, newest first

#Quick start

bash
export CREWLINK=https://go.crewlink.ai

# 1. A company's services (no key needed)
curl $CREWLINK/v1/public/northline

# 2. Open times for a repair visit at an address, the next 7 days
curl -X POST $CREWLINK/v1/public/northline/availability \
  -H 'content-type: application/json' \
  -d '{"serviceId": 2, "lat": 41.955, "lng": -87.675, "days": 7}'

# 3. Today's schedule from the office side (company API key)
curl $CREWLINK/v1/office/schedule -H "authorization: Bearer $KEY"

Then follow Booking page to hold a time and book it.

#Base URL and environments

Environment Base URL
Live https://go.crewlink.ai: the only host today. Real companies and the test companies share it; ask CrewLink for an API key for a test company, and platforms get the acme-test sandbox (Platforms: sandbox). Payments are Stripe test mode until live payments are switched on
Later https://api.crewlink.ai is reserved for the API and does not resolve yet; don't use it

All paths start with /v1. Requests and responses are JSON (content-type: application/json), UTF-8, over HTTPS only.

#Authentication

Public booking-page endpoints and the homeowner's links need no key. A website that calls them can send an optional public integration key (X-CrewLink-Key) to get its own limits; it is not a credential (see Calling from a browser). Everything else takes a bearer token:

Authorization: Bearer <id>.<secret>

There are four kinds of token. The first three look the same; CrewLink tells them apart by the id.

Token What it is Acts as Lifetime Get it
Company API key <company id>.<secret> The owner Until rotated (POST /v1/office/api-key/rotate, owner only) From CrewLink when your company is set up
User token <user id>.<secret> That user, with their role Until the user is deactivated or re-keyed (re-keying also ends their sessions) POST /v1/office/users (owner only), shown once; see Office: users
Session <session id>.<secret> That user, with their role 30 days Sign-in (emailed code or passkey)
Platform key pl_<platform id>.<secret>, sent with X-CrewLink-Company: <company slug or id> The owner of a company that granted that platform access, within the scopes it granted (read, write, webhooks); never users, keys, settings, plan, money, messages or the field app Until CrewLink replaces it; a company's revoke ends it for that company at once From CrewLink, for products built on it; see Platforms

CrewLink's own staff can also hold a 1-hour act-as session for your company (support, with your say-so): it acts as your owner, can't move money or mint keys or tokens, and every request it makes is logged with their name. GET /v1/office/me shows actingAs while one is in use. See Admin.

A platform key with no X-CrewLink-Company gets 400 company_required; a company that hasn't granted it answers 404; a call outside the grant's scopes gets 403 scope. Every platform call is logged, and its rate limit is 1,200 a minute per key (instead of per IP).

Use the company API key for server-to-server integrations (website back end, CRM sync). Use user tokens for a device or integration that should act as one person (a tech's tablet, a dispatcher tool). Use sessions for apps people sign in to. Keep every token on a server or in the device's secure storage; never put one in web page code. The company API key acts as the owner: anyone who reads it from a page can see and change your whole schedule. The only key that belongs in a page is the integration key.

#Roles

Role Can
owner Everything, including users, company settings, billing plan, webhooks, the company API key and CrewConnect pay terms
dispatcher Office work: schedule, jobs, team, branches, services, time off, emergencies, requests, trips, invoices, reports, CrewConnect jobs. Not users, settings, plan, webhooks, the API key or pay terms
field_manager Everything a dispatcher may do, for a team of dispatchers (teamLeadId): sees what the team filed and invites dispatchers to it
tech The field app endpoints, for jobs on their own schedule only

Each office user also has a scope: company (the default for owners and dispatchers), team (the default for field managers) or own. People with team or own scope see other jobs as busy time, change only jobs in their scope, and get 403 scoped on company-wide endpoints (reports, KPIs, exports, money, settings, the live board, clients, webhooks). See Roles and scopes.

A missing or invalid token gets 401 unauthorized. A valid token whose role isn't allowed gets 403 forbidden. A token only ever sees its own company's data: ids from another company answer 404 not_found, never 403, so they reveal nothing.

#Conventions

#Times

Every company has an IANA time zone (for example America/Chicago), returned as timezone by GET /v1/public/{slug}, and each branch can have its own (a Chicago company's Denver branch runs on America/Denver, its London branch on Europe/London). A job is on the clock of the branch serving its address; a tech's working hours, days and "today" are on their branch's clock, including across daylight-saving changes. Answers with wall-clock times say which zone in timezone. A company in one zone sees no difference.

Responses give a time two ways:

json
{ "start": "2026-10-13T19:30:00.000Z", "local": "2026-10-13T14:30" }
  • start is the real instant, ISO 8601 in UTC. Store and compare this one.
  • local is the wall clock where the job is (timezone), YYYY-MM-DDTHH:mm, with no offset. Show this one to people.

Requests take ISO 8601 instants with an offset (2026-10-13T19:30:00Z or 2026-10-13T14:30:00-05:00). The simplest correct thing is to send back a start exactly as CrewLink gave it to you. Office bookings and moves also take local (the wall clock where the job is) instead. Dates (from, date, YYYY-MM-DD) are local calendar dates in the zone they're about: where the address is for availability, the tech's branch for a tech's day.

#Countries, phone numbers and distances

A company has a country (US, GB, DE, ...) and each branch may have its own. Texts go to mobile numbers in those countries only (a US company with a London branch texts US, Canadian and UK numbers); a number without + and a country code is read the way the job's country writes it. Address search with a slug looks in those countries. Distances in the API are always metric (km, areaKm, ...PerKmCents); show them in the company's units (mi or km, from GET /v1/public/{slug} or GET /v1/office/me).

A few office fields describe a working day as minutes from local midnight: 480 is 8:00 AM, 1020 is 5:00 PM. They are called out where they appear.

Online start times are offered on a 15-minute grid, and nothing sooner than one hour from now.

#Money

Amounts are integers in the currency's minor unit (cents for USD) with an ISO 4217 code alongside:

json
{ "priceCents": 8900, "depositCents": 4900, "currency": "USD" }

8900 with USD is $89.00. Never send or expect decimals.

#Ids

Companies, users, sessions, webhook endpoints, CrewConnect connections and photos have UUID ids (strings). Jobs, bookings, holds, services, people (techs), blocks, invoices, requests, trips and most other records have integer ids. A booking id, a hold id and a job id are the same number for the same job: a hold becomes a booking, and the booking is a job on the office schedule.

#Errors

Every error has the same body:

json
{ "error": "slot_taken", "message": "That time was just taken. Pick another." }

Branch on error, the stable machine code. message is a plain-English sentence you can show a person; its wording may change.

Status error When
400 bad_request The request couldn't be read (for example, malformed JSON)
401 unauthorized No token, or a token that isn't valid
401 bad_code Sign-in: the code is wrong, used or expired
401 bad_passkey, challenge_expired Passkey sign-in: the passkey didn't verify, or the loginId is used, unknown or older than 5 minutes
400 company_required A platform key without X-CrewLink-Company
403 forbidden Your role can't do this
403 scope A platform key whose grant doesn't include the scope this needs
403 scoped A user with team or own scope called a company-wide endpoint
403 not_yours CrewConnect: only the other side of the connection may change that setting
403 not_allowed CrewConnect: the sender didn't allow passing this job on
404 not_found No such record for you: unknown id, another company's id, a wrong or expired link token
409 slot_taken The time isn't free any more (another booking took it, or no qualified tech can make it)
409 too_soon The time is less than an hour from now
409 too_late A homeowner tried to cancel or move inside the company's cutoff (for example 24 hours); or a CrewConnect sender tried to take back a job that's done or has started
409 in_the_past An office booking or move to a time that has passed
409 not_offered A homeowner picked a time that wasn't one of the times offered
409 already Already done: a job stage that has been reached, a review already left
409 changed A suggested trip no longer fits the schedule; ask for fresh suggestions
409 exists CrewConnect: you already have a connection with that company
409 not_pending, not_active CrewConnect: the connection isn't in the state the action needs
409 nothing_to_accept, terms_changed CrewConnect pay terms: none to accept, or they changed since you read them
409 not_enough Inventory: not that many parts in that place
409 not_on_site Field app: check in only after arriving
409 no_visit_today Field app: a check-in on a job that isn't on your schedule today
409 in_progress The first request with this Idempotency-Key is still running
409 duplicate_external_ref An external id (externalRefs) is already on another job, person or block; the body names it (jobId, personId or blockId)
409 no_phone Field app: the tech or the customer has no usable phone number for a masked call
409 no_one_today Emergencies: no qualified tech can get there today
409 not_sendable Invoices: only a draft or sent invoice can be sent
409 not_voidable Invoices: a paid invoice can't be voided
410 hold_expired The hold ran out before it was booked
422 invalid_request The body, query or path failed validation (missing field, wrong type, out of range); message says which
422 invalid_url Webhooks: the URL isn't allowed
422 invalid_at Field app: at is more than 24 hours ago or in the future
422 idempotency_key_reused The Idempotency-Key was already used for a different request
429 rate_limited Too many requests from your IP (or integration key); wait for Retry-After
429 too_many_holds Too many live holds from your IP (or integration key)
429 slow_down Address search: one new lookup a second
500 server_error Something went wrong on our side. Retry with backoff
502 payments_unavailable The payment provider couldn't be reached; try again in a few minutes
503 not_configured A feature (calendar sync, masked calling) isn't switched on for this environment

Each guide lists the codes each endpoint can return.

#Health

GET /v1/health needs no key and returns no data: 200 { "ok": true, "db": "ok", "release": "cl-…", "uptimeSeconds": 5321, "time": "…" } while the API and its database answer, 503 with "ok": false, "db": "down" when the database doesn't. Point uptime monitors at it (never cached).

#Rate limits

Limits are per client IP address (IPv6: per /64), per minute, counted separately for each endpoint listed. With a valid integration key the four public endpoints marked below count per key instead:

Endpoint Per IP Per integration key
Every endpoint (default) 600 a minute
POST /v1/auth/start 5 a minute (and 5 codes an hour per person)
POST /v1/auth/verify 10 a minute
POST /v1/auth/passkeys/login/options 10 a minute
POST /v1/auth/passkeys/login 10 a minute
POST /v1/public/{slug}/holds 30 a minute 300 a minute
POST /v1/public/{slug}/bookings 30 a minute 300 a minute
POST /v1/public/{slug}/requests 30 a minute 300 a minute
GET /v1/public/geocode 30 a minute, and one new lookup a second (cached answers are always served) 300 a minute; still one new lookup a second per visitor IP

On top of the request rates, one IP can have at most 3 live holds at a time, or one integration key 50 (a hold stops counting when it is booked or expires).

Over a limit, the API answers 429 with a Retry-After header (seconds) and X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers. Wait at least Retry-After before trying again.

If your server calls the booking endpoints on behalf of many website visitors, they all share your server's IP and the per-IP limits. Send your company's integration key and they count per key instead. If 300 a minute or 50 live holds won't fit your traffic, talk to CrewLink before launch.

#Calling from a browser

A company's own website can call the public booking endpoints straight from the visitor's browser: no server of yours in between. Two settings, both owner-only and both in the office app under Settings > Online booking:

  1. Allowed origins (CORS). List your website's https origins (https://www.yourcompany.com, up to 20) with PATCH /v1/office/settings allowedOrigins. Browsers only hand CrewLink's answers to pages on those sites.
  2. The integration key (optional, recommended). Make one with POST /v1/office/integration-key/rotate and send it as X-CrewLink-Key on every booking call.

#CORS rules

Endpoints Which pages may call them
GET /v1/public/{slug}, POST /v1/public/{slug}/availability, /holds, /bookings, /requests Pages on that company's allowedOrigins
GET /v1/public/geocode Pages on the allowedOrigins of the company in slug (or of the integration key's company). Send slug
The homeowner's links: /v1/bookings/{id} (view, move, cancel), /v1/public/offers/{jobId}, /v1/public/invoices/{id}, /v1/public/reviews/{jobId} Any page (Access-Control-Allow-Origin: *): the secret token in the link authorizes them, and they only touch that one homeowner's booking
GET /v1/openapi.json Any page
Everything else (office, field app, CrewConnect, sign-in) No page. Call them from a server
  • Allowed: GET and POST, with the request headers Content-Type and X-CrewLink-Key. Authorization is not allowed from a page, so no bearer token can be sent cross-site.
  • Preflight (OPTIONS) is answered with 204 and cached by the browser for 10 minutes. A page whose origin isn't listed gets no CORS headers (its preflight answers 404), and the browser blocks the call.
  • Pages can read Retry-After and the X-RateLimit-* headers.
  • No cookies are involved (Access-Control-Allow-Credentials is never sent); don't use credentials: 'include'.
  • Changes to allowedOrigins apply to the next request.

The hosted booking page and the iframe (Embed on your website) need none of this.

#The integration key

A public key, pk_..., that ties website traffic to your company:

  • Safe to put in your website's code. It grants no access to data and opens no endpoint; anyone who copies it can only do what any visitor can already do on your booking page.
  • What it changes: holds, bookings, out-of-area requests and address search count against the key instead of each visitor's IP, with higher ceilings (300 a minute per endpoint, 50 live holds), so many visitors behind one office network or a server-side caller aren't throttled as one person. Your traffic is attributed to your company.
  • Only on your own endpoints. On another company's slug, or after it is replaced, the key is ignored and the request counts per IP as usual. A wrong key is never an error.
  • One at a time. Making a new one stops the old one at once. Since anyone can read it from your page, someone could spend its limits; if that happens, make a new one and ask CrewLink.
  • Never put the company API key (or any bearer token) in a page. It acts as the owner of your account.
js
// In your website's script. The key is public; the company API key never goes here.
const CREWLINK = 'https://go.crewlink.ai';
const headers = { 'content-type': 'application/json', 'x-crewlink-key': 'pk_<integration key>' };
const slots = await fetch(`${CREWLINK}/v1/public/northline/availability`, {
  method: 'POST', headers, body: JSON.stringify({ serviceId: 2, lat: 41.955, lng: -87.675, days: 7 }),
}).then((r) => r.json());

The full sequence is in Booking page: build it into your site.

#Idempotency and retries

Every write with a token takes an Idempotency-Key header (POST, PUT, PATCH and DELETE on the office, CrewConnect and platform endpoints; the field app's writes: job stages, check-ins, location, notes, parts): a retry with the same key and body within 24 hours returns the first response (with Idempotent-Replayed: true) instead of acting twice; the same key with a different body is 422 idempotency_key_reused; while the first still runs, 409 in_progress. Keys are per user (or per company API key, or per platform and company). Secrets aren't kept for replays: a replayed answer has fields such as manageToken and holdSecret empty, and calls that mint a credential (key rotations, user tokens, webhook secrets) ignore the header. See Field app: offline taps and retries. Jobs, people and blocks also take your own ids (externalRefs), which can't be used twice. Beyond that the flows are designed so retries are safe or detectable:

  • Holds last 5 minutes (expiresAt). A hold that isn't booked simply expires and frees the time; there is nothing to clean up.
  • Bookings consume a hold once. Retrying POST .../bookings with the same holdId after a success returns 404 (the hold is no longer held), so a duplicate booking can't happen. If a booking request times out, look the booking up rather than holding again.
  • Cancels are safe to repeat: a second cancel answers 404 because there is no active booking left.
  • Job stages only move forward; repeating one answers 409 already.
  • Reviews are one per job; a second answers 409 already.
  • Webhooks may arrive more than once. Each delivery carries a unique id (CrewLink-Delivery header and id in the body): store the ids you've processed and skip repeats. See Webhooks.

For 5xx answers and network errors, retry with exponential backoff (for example 1 s, 2 s, 4 s, up to 5 tries). Don't retry 4xx answers unchanged, except 429 after Retry-After.

#Text messages

CrewLink texts homeowners when a booking is confirmed, reminded (24 hours before), moved, cancelled, when the tech is on the way, when an invoice is sent and after the job (a review link). Texts go to mobile numbers in the company's countries and its branches' (US and Canada for a US company; see Countries, phone numbers and distances); a booking with another number still succeeds, it just gets no texts. The texts carry the company's name (or, for CrewConnect work, the brand the sender chose).

#Versioning

The API is versioned in the path (/v1). Within v1 we add endpoints, optional request fields, response fields and webhook events without notice; build clients that ignore fields and events they don't know. Anything that would break a correct client gets a new version and a notice in the changelog.

CrewLink API · Reference · Changelog