Skip to content

Developers

The Emerald Field OS API

Read crew time, pay lines, jobs, dailies and production, and get events pushed to you with signed webhooks. Available on Pro.

Base URL https://emeraldfieldos.com/api/v1

curl "https://emeraldfieldos.com/api/v1/shifts?from=2026-10-12&to=2026-10-18" \
  -H "Authorization: Bearer efos_live_…"
200 OK · application/json (example)
{
  "data": [
    {
      "id": "shf_01J…", "employeeId": "emp_01J…", "projectId": "prj_01J…",
      "workDate": "2026-10-15", "crewId": "crw_01J…", "status": "SUPERVISOR_APPROVED", "entryMethod": "CREW",
      "totalMinutes": 512, "breakMinutes": 30, "flags": [],
      "allocations": [
        { "costCodeId": "cc_03300", "minutes": 360, "equipmentId": null },
        { "costCodeId": "cc_02600", "minutes": 152, "equipmentId": "eq_EX07" }
      ],
      "updatedAt": "2026-10-15T22:04:11Z"
    }
  ],
  "nextCursor": "c_8f2…"
}

Authentication

Create an API key in Settings → API & webhooks. Keys are shown once; send them as a bearer token. Each key can read all of its company's data, so store it like a password.

Request header
Authorization: Bearer efos_live_…

Connect to your AI

Emerald Field OS is a remote MCP server. Add it to Claude (Settings → Connectors → Add custom connector), ChatGPT, Cursor or Claude Code and ask about jobs, crews, hours, dailies and production in plain English. People sign in with OAuth and the AI sees exactly what their role sees; it can create drafts but never approves time, syncs payroll or publishes the schedule. Headless clients can use an API key as the bearer token. Pro plan.

https://emeraldfieldos.com/api/mcp

Setup steps for each tool are in Settings → AI connectors.

Conventions & errors

Units
Money is integer cents. Durations are integer minutes. Dates are YYYY-MM-DD; timestamps are ISO 8601 UTC.
Pagination
Lists return { data, nextCursor }. Pass cursor for the next page; limit is 1–200 (default 50).
Rate limit
600 requests per minute per key. Watch X-RateLimit-Remaining; a 429 includes Retry-After.
Errors
Always { "error": { "code", "message" } }, with codes such as UNAUTHENTICATED (401), FORBIDDEN (403), VALIDATION (422) and RATE_LIMITED (429).

Endpoints

All API endpoints
Endpoint
GET/employees
GET/projects
GET/cost-codes
GET/shifts
GET/pay-lines
GET/dailies
GET/production
POST/webhooks

GET/employees

List employees.

Query parameters

limitintegerPage size (1–200, default 50)
cursorstring`nextCursor` from the previous page
status"ACTIVE" | "INACTIVE" | "TERMINATED" | "PENDING_SETUP"

Response: { data: Employee[], nextCursor }

idrequiredstring
employeeNumberrequiredstring
firstNamerequiredstring
lastNamerequiredstring
preferredNamerequiredstring | null
phonerequiredstring | null
emailrequiredstring | null
languagerequired"EN" | "ES"
statusrequired"ACTIVE" | "INACTIVE" | "TERMINATED" | "PENDING_SETUP"
hireDaterequiredstring | null
defaultClassificationIdrequiredstring | null
externalIdsrequiredobject
updatedAtrequiredstringISO 8601 timestamp (UTC)

GET/projects

List projects (jobs).

Query parameters

limitintegerPage size (1–200, default 50)
cursorstring`nextCursor` from the previous page
status"BIDDING" | "ACTIVE" | "ON_HOLD" | "COMPLETED"Project status

Response: { data: Project[], nextCursor }

idrequiredstring
numberrequiredstring
namerequiredstring
customerNamerequiredstring | null
statusrequiredstring
timezonerequiredstring
startDaterequiredstring | null
endDaterequiredstring | null
isPrevailingWagerequiredboolean
isUnionrequiredboolean
externalIdsrequiredobject
updatedAtrequiredstringISO 8601 timestamp (UTC)

GET/cost-codes

List cost codes.

Query parameters

limitintegerPage size (1–200, default 50)
cursorstring`nextCursor` from the previous page
projectIdstring

Response: { data: CostCode[], nextCursor }

idrequiredstring
projectIdrequiredstring
coderequiredstring
descriptionrequiredstring
costTyperequired"LABOR" | "EQUIPMENT" | "MATERIAL" | "SUB" | "OTHER"
unitrequiredstring | null
budgetQtyrequirednumber | null
budgetMinutesrequiredinteger | null
isActiverequiredboolean
isTMrequiredboolean
updatedAtrequiredstringISO 8601 timestamp (UTC)

GET/shifts

List shifts with cost-code allocations.

Query parameters

limitintegerPage size (1–200, default 50)
cursorstring`nextCursor` from the previous page
fromstringFirst work date (inclusive)
tostringLast work date (inclusive)
status"OPEN" | "SUBMITTED" | "SUPERVISOR_APPROVED" | "PAYROLL_APPROVED" | "EXPORTED" | "LOCKED"
projectIdstring

Response: { data: Shift[], nextCursor }

idrequiredstring
employeeIdrequiredstring
projectIdrequiredstring
workDaterequiredstringCalendar date, YYYY-MM-DD
classificationIdrequiredstring
crewIdrequiredstring | null
statusrequiredstring
entryMethodrequiredstring
totalMinutesrequiredinteger
breakMinutesrequiredinteger
injuryAttestationrequiredstring | null
payPeriodIdrequiredstring | null
flagsrequiredstring[]
allocationsrequiredobject[]
updatedAtrequiredstringISO 8601 timestamp (UTC)

GET/pay-lines

List computed pay lines. Rate-engine output per shift allocation. Amounts are integer cents; minutes are integers.

Query parameters

limitintegerPage size (1–200, default 50)
cursorstring`nextCursor` from the previous page
periodIdstringPay period id; omit for all periods

Response: { data: PayLine[], nextCursor }

idrequiredstring
shiftIdrequiredstring
employeeIdrequiredstring
workDaterequiredstringCalendar date, YYYY-MM-DD
payTyperequiredstring
minutesrequiredinteger
rateCentsrequiredinteger
fringeCentsrequiredinteger
amountCentsrequiredinteger
costCodeIdrequiredstring
classificationIdrequiredstring
isPrevailingrequiredboolean

GET/dailies

List daily reports.

Query parameters

limitintegerPage size (1–200, default 50)
cursorstring`nextCursor` from the previous page
projectIdstring
fromstringCalendar date, YYYY-MM-DD
tostringCalendar date, YYYY-MM-DD
status"DRAFT" | "SUBMITTED" | "SIGNED" | "SENT"

Response: { data: DailyReport[], nextCursor }

idrequiredstring
projectIdrequiredstring
reportDaterequiredstringCalendar date, YYYY-MM-DD
statusrequiredstring
workPerformedrequiredstring | null
safetyNotesrequiredstring | null
delaysrequiredany | null
weatherrequiredany | null
manpowerrequiredany | null
signedAtrequiredstring | null
pdfUrlrequiredstring | null
updatedAtrequiredstringISO 8601 timestamp (UTC)

GET/production

List production entries.

Query parameters

limitintegerPage size (1–200, default 50)
cursorstring`nextCursor` from the previous page
projectIdstring
fromstringCalendar date, YYYY-MM-DD
tostringCalendar date, YYYY-MM-DD

Response: { data: ProductionEntry[], nextCursor }

idrequiredstring
projectIdrequiredstring
costCodeIdrequiredstring
workDaterequiredstringCalendar date, YYYY-MM-DD
qtyrequirednumber
unitrequiredstring
locationrequiredstring | null
percentCompleterequirednumber | null
dailyReportIdrequiredstring | null
updatedAtrequiredstringISO 8601 timestamp (UTC)

POST/webhooks

Subscribe a webhook endpoint. Deliveries are POSTed as JSON and signed with `X-EmeraldField-Signature: t=<unix>,v1=<hex HMAC-SHA256 of "t.body">`.

Request body (JSON)

urlrequiredstring
eventsrequired"*" | "shift.approved" | "period.synced" | "daily.submitted" | "form.flagged" | "incident.created"[]Event names, or ["*"] for all

Response: WebhookSubscription

idrequiredstring
urlrequiredstring
eventsrequiredstring[]
activerequiredboolean
secretrequiredstringSigning secret — returned once at creation
createdAtrequiredstringISO 8601 timestamp (UTC)

Webhooks

Add endpoints in Settings or with POST /webhooks. We POST JSON for shift.approved, period.synced, daily.submitted, form.flagged and incident.created. Respond with any 2xx within 10 seconds; anything else is retried up to 5 times with exponential backoff.

Example delivery
POST /your-endpoint
X-EmeraldField-Signature: t=1760550000,v1=5f2b…
X-EmeraldField-Event: shift.approved

{ "id": "…", "event": "shift.approved", "createdAt": "2026-10-15T22:04:11Z",
  "companyId": "…", "data": { "shiftId": "…", "employeeId": "…", "workDate": "2026-10-15" } }

Verify the signature

verify.ts
import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody: the exact request body string; header: X-EmeraldField-Signature
export function verify(rawBody: string, header: string, secret: string) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // replay window
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
  const got = Buffer.from(v1, "hex");
  return got.length === expected.length && timingSafeEqual(got, expected);
}
API reference · Emerald Field OS