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_…"{
"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.
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/mcpSetup 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 }. Passcursorfor the next page;limitis 1–200 (default 50). - Rate limit
- 600 requests per minute per key. Watch
X-RateLimit-Remaining; a 429 includesRetry-After. - Errors
- Always
{ "error": { "code", "message" } }, with codes such as UNAUTHENTICATED (401), FORBIDDEN (403), VALIDATION (422) and RATE_LIMITED (429).
Endpoints
| Endpoint | Description |
|---|---|
| GET/employees | List employees |
| GET/projects | List projects (jobs) |
| GET/cost-codes | List cost codes |
| GET/shifts | List shifts with cost-code allocations |
| GET/pay-lines | List computed pay lines |
| GET/dailies | List daily reports |
| GET/production | List production entries |
| POST/webhooks | Subscribe a webhook endpoint |
GET/employees
List employees.
Query parameters
| limit | integer | Page size (1–200, default 50) |
| cursor | string | `nextCursor` from the previous page |
| status | "ACTIVE" | "INACTIVE" | "TERMINATED" | "PENDING_SETUP" |
Response: { data: Employee[], nextCursor }
| idrequired | string | |
| employeeNumberrequired | string | |
| firstNamerequired | string | |
| lastNamerequired | string | |
| preferredNamerequired | string | null | |
| phonerequired | string | null | |
| emailrequired | string | null | |
| languagerequired | "EN" | "ES" | |
| statusrequired | "ACTIVE" | "INACTIVE" | "TERMINATED" | "PENDING_SETUP" | |
| hireDaterequired | string | null | |
| defaultClassificationIdrequired | string | null | |
| externalIdsrequired | object | |
| updatedAtrequired | string | ISO 8601 timestamp (UTC) |
GET/projects
List projects (jobs).
Query parameters
| limit | integer | Page size (1–200, default 50) |
| cursor | string | `nextCursor` from the previous page |
| status | "BIDDING" | "ACTIVE" | "ON_HOLD" | "COMPLETED" | Project status |
Response: { data: Project[], nextCursor }
| idrequired | string | |
| numberrequired | string | |
| namerequired | string | |
| customerNamerequired | string | null | |
| statusrequired | string | |
| timezonerequired | string | |
| startDaterequired | string | null | |
| endDaterequired | string | null | |
| isPrevailingWagerequired | boolean | |
| isUnionrequired | boolean | |
| externalIdsrequired | object | |
| updatedAtrequired | string | ISO 8601 timestamp (UTC) |
GET/cost-codes
List cost codes.
Query parameters
| limit | integer | Page size (1–200, default 50) |
| cursor | string | `nextCursor` from the previous page |
| projectId | string |
Response: { data: CostCode[], nextCursor }
| idrequired | string | |
| projectIdrequired | string | |
| coderequired | string | |
| descriptionrequired | string | |
| costTyperequired | "LABOR" | "EQUIPMENT" | "MATERIAL" | "SUB" | "OTHER" | |
| unitrequired | string | null | |
| budgetQtyrequired | number | null | |
| budgetMinutesrequired | integer | null | |
| isActiverequired | boolean | |
| isTMrequired | boolean | |
| updatedAtrequired | string | ISO 8601 timestamp (UTC) |
GET/shifts
List shifts with cost-code allocations.
Query parameters
| limit | integer | Page size (1–200, default 50) |
| cursor | string | `nextCursor` from the previous page |
| from | string | First work date (inclusive) |
| to | string | Last work date (inclusive) |
| status | "OPEN" | "SUBMITTED" | "SUPERVISOR_APPROVED" | "PAYROLL_APPROVED" | "EXPORTED" | "LOCKED" | |
| projectId | string |
Response: { data: Shift[], nextCursor }
| idrequired | string | |
| employeeIdrequired | string | |
| projectIdrequired | string | |
| workDaterequired | string | Calendar date, YYYY-MM-DD |
| classificationIdrequired | string | |
| crewIdrequired | string | null | |
| statusrequired | string | |
| entryMethodrequired | string | |
| totalMinutesrequired | integer | |
| breakMinutesrequired | integer | |
| injuryAttestationrequired | string | null | |
| payPeriodIdrequired | string | null | |
| flagsrequired | string[] | |
| allocationsrequired | object[] | |
| updatedAtrequired | string | ISO 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
| limit | integer | Page size (1–200, default 50) |
| cursor | string | `nextCursor` from the previous page |
| periodId | string | Pay period id; omit for all periods |
Response: { data: PayLine[], nextCursor }
| idrequired | string | |
| shiftIdrequired | string | |
| employeeIdrequired | string | |
| workDaterequired | string | Calendar date, YYYY-MM-DD |
| payTyperequired | string | |
| minutesrequired | integer | |
| rateCentsrequired | integer | |
| fringeCentsrequired | integer | |
| amountCentsrequired | integer | |
| costCodeIdrequired | string | |
| classificationIdrequired | string | |
| isPrevailingrequired | boolean |
GET/dailies
List daily reports.
Query parameters
| limit | integer | Page size (1–200, default 50) |
| cursor | string | `nextCursor` from the previous page |
| projectId | string | |
| from | string | Calendar date, YYYY-MM-DD |
| to | string | Calendar date, YYYY-MM-DD |
| status | "DRAFT" | "SUBMITTED" | "SIGNED" | "SENT" |
Response: { data: DailyReport[], nextCursor }
| idrequired | string | |
| projectIdrequired | string | |
| reportDaterequired | string | Calendar date, YYYY-MM-DD |
| statusrequired | string | |
| workPerformedrequired | string | null | |
| safetyNotesrequired | string | null | |
| delaysrequired | any | null | |
| weatherrequired | any | null | |
| manpowerrequired | any | null | |
| signedAtrequired | string | null | |
| pdfUrlrequired | string | null | |
| updatedAtrequired | string | ISO 8601 timestamp (UTC) |
GET/production
List production entries.
Query parameters
| limit | integer | Page size (1–200, default 50) |
| cursor | string | `nextCursor` from the previous page |
| projectId | string | |
| from | string | Calendar date, YYYY-MM-DD |
| to | string | Calendar date, YYYY-MM-DD |
Response: { data: ProductionEntry[], nextCursor }
| idrequired | string | |
| projectIdrequired | string | |
| costCodeIdrequired | string | |
| workDaterequired | string | Calendar date, YYYY-MM-DD |
| qtyrequired | number | |
| unitrequired | string | |
| locationrequired | string | null | |
| percentCompleterequired | number | null | |
| dailyReportIdrequired | string | null | |
| updatedAtrequired | string | ISO 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)
| urlrequired | string | |
| eventsrequired | "*" | "shift.approved" | "period.synced" | "daily.submitted" | "form.flagged" | "incident.created"[] | Event names, or ["*"] for all |
Response: WebhookSubscription
| idrequired | string | |
| urlrequired | string | |
| eventsrequired | string[] | |
| activerequired | boolean | |
| secretrequired | string | Signing secret — returned once at creation |
| createdAtrequired | string | ISO 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.
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
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);
}