FieldCamp
Resources

Jobs | FieldCamp API

Reference for the FieldCamp Jobs API — create, schedule, filter, and update field service jobs with JSON requests on the v1 endpoints.

The FieldCamp Jobs API is the headline resource for scheduled work: a client, an address, a time window, a team, and one or more line items. If you are building a booking system, a dispatch tool, or a sync to another platform, the /api/v1/jobs endpoints are the primary surface you will integrate against. This page covers the current request shape, filter parameters, status lifecycle, and common patterns for working with job and visit statuses.

What changed in v1

The Jobs API now lives under /api/v1/jobs and accepts a normal application/json body — not multipart/form-data. Earlier internal versions of the docs described a two-field form payload (jobData + notes); that pattern is no longer required. Send a single JSON object and you are done.

If you previously saw 400 jobNumber is required responses despite including jobNumber in your payload, you were almost certainly hitting the old multipart parser. Switching to application/json against /api/v1/jobs resolves it.

Authentication and base URL

All requests use the standard FieldCamp API base URL and your API key. See Authentication for header conventions, Errors for the error envelope, and Rate limits for throttling rules. Idempotent retries are documented in Idempotency.

Most important things to know

POST /api/v1/jobs uses JSON. Set Content-Type: application/json and send the job object directly. Do not stringify a jobData field inside a form payload.

assignedToTeams takes user IDs, not team IDs. The field name is historical and is preserved for compatibility.

Read them from GET /api/teams with your key on the Authorization header (that legacy route does not read X-Api-Key). There is no /api/v1/team endpoint — if you had that in a snippet, it 404s. See the Team resource for the response shape.

Datetimes are UTC ISO-8601 — do not encode an offset into startDateTime. Always convert your local time to UTC before sending.

Create a job

POST /api/v1/jobs accepts a JSON body with the following important fields:

  • clientIdrequired. The FieldCamp client this job belongs to. See the Clients resource.
  • type"one-off" or "recurring". One-off jobs run once; recurring jobs spawn child visits on a schedule. Also accepted as jobType. Defaults to "one-off".
  • status — the pipeline stage key to start on, e.g. "scheduled". Also accepted as jobStatus. Defaults to "draft" when scheduleLater is true, otherwise "scheduled". The valid keys are your account's own — read them from GET /api/v1/objects/job/schema.
  • priority — string priority label used by the AI Dispatcher and dashboard filters.
  • startDateTime and endDateTime — UTC ISO-8601 timestamps.
  • scheduleLater / anyTime — booleans. scheduleLater creates the job without a time window; anyTime marks it as not time-boxed.
  • assignedToTeams — array of team-member ids.
  • jobNumber — your own reference. Alphanumeric values are accepted.
  • jobAddress / jobPhone — the service address and contact for this job.
  • subTotal, discount, tax, total — money fields, sent as numbers.
  • internalNotes — internal-only text. Also accepted as notes.
  • targetObjectSlug + targetRecordId — link the job to a custom-object record such as an asset or unit. Both are required together and are validated against your account. See Custom objects.

Anything outside this list returns 400 with the offending key named — the API never accepts and silently discards a field. In particular there is no lineItems field on a job: products and services are line items on an estimate or invoice, not on the job record.

  • subTotal, discount, tax, total — money totals as numbers (in your account's base currency).
  • notes — a plain string with any free-form notes. No longer a separate form field.

The money fields are stored exactly as you send them — this endpoint does no arithmetic, and there are no line items on a job to compute from. Omit subTotal, discount, tax or total and each is stored as 0, so a job created without them reports as a zero-value job. Send the figures your own system holds, or build the priced document as an estimate or invoice instead.

Example request

curl -X POST https://api.fieldcamp.ai/api/v1/jobs \
  -H "Authorization: Bearer $FIELDCAMP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "one-off",
    "status": "scheduled",
    "priority": "normal",
    "clientId": "cli_01H...",
    "startDateTime": "2026-06-03T14:00:00.000Z",
    "endDateTime": "2026-06-03T16:00:00.000Z",
    "assignedToTeams": ["usr_01H..."],
    "subTotal": 250,
    "discount": 0,
    "tax": 22.5,
    "total": 272.5,
    "notes": "Gate code 4412. Please call on arrival."
  }'

List and filter jobs

GET /api/v1/jobs supports the following query parameters:

  • page — page number, 1-based. Defaults to 1.
  • limit — rows per page. Defaults to 30; values above 100 are not rejected — they are silently clamped to 100 (you still get 200 with 100 rows). Read meta.total for the real count and page through the rest.
  • sort — order the result as field:direction, e.g. sort=total:desc or sort=startDateTime:asc. Direction defaults to desc. Sortable fields: createdAt, updatedAt, startDateTime, endDateTime, jobNumber, total, jobStatus, priority. Any other field returns 400.
  • search — free-text match on the job number and the client's first name, last name, or company name. It does not search addresses, descriptions, or line items.
  • clientId — return only jobs for a specific client.
  • status — filter by job status, matched case-insensitively (completed and Completed both work). The valid values are your own pipeline's stage keys, not a fixed global list — read them from GET /api/v1/objects/job/schema. Also accepted as jobStatus for older integrations.
  • since — return jobs whose updatedAt is at or after this ISO-8601 timestamp (the incremental-sync cursor). A value that is not a date returns 400 rather than being ignored. Include a timezone so the cutoff is unambiguous — send it in UTC (2026-08-26T07:04:20Z) or with an explicit offset (2026-08-26T07:04:20-06:00). A value without a timezone (e.g. 2026-08-26T07:04:20) is read as UTC; if you're passing local wall-clock time, add the timezone parameter so it's converted correctly (otherwise the cutoff lands earlier than you intend and rows from before it come through).
  • timezone — an IANA timezone name (e.g. America/Denver) used to interpret a since that has no timezone of its own. When set, a naive since is read as local wall-clock time in this zone and converted to UTC. Ignored when since already carries a Z or an offset.

Reading the response

Every list endpoint returns the rows in data and the pagination state in meta:

{
  "success": true,
  "data": [ /* … jobs … */ ],
  "meta": { "page": 1, "limit": 30, "total": 14970 }
}

Use meta.total to work out how many pages there are — Math.ceil(total / limit). There is no next cursor; increment page until you have seen total rows.

limit is capped at 100, so a large account needs many round trips to walk the full list — 14,970 jobs is 150 requests. For a dashboard, filter with status, clientId or since first rather than paging through everything. See Incremental sync for the full recipe.

Backfill once

Call GET /api/v1/jobs?sort=updatedAt:asc (no since) and paginate through every page. Sorting by updatedAt is what makes the sync reliable — the last row on the last page becomes your starting cursor.

Sync incrementally

On every subsequent run, call GET /api/v1/jobs?since={last_updated_at}&sort=updatedAt:asc. Always keep sort=updatedAt:asc — with the default sort (createdAt desc) rows come back in creation order, so combining since with page/limit returns what looks like old data and you can't track a clean cursor. After each run, save the last row's updatedAt as your next since.

Filter by status

For a dispatch board, narrow to the stage keys that mean active work on your account — commonly status=scheduled and status=in-progress. Confirm the keys your account actually uses with GET /api/v1/objects/job/schema; one request per status, as status takes a single value.

Update a job

PUT /api/v1/jobs/{id} is a partial update — send only the fields you want to change — but its contract is narrower than create. It accepts exactly these fields:

jobAddress, jobPhone, jobType, startDateTime, endDateTime, assignedToTeams, scheduleLater, anyTime, subTotal, discount, tax, total, priority, internalNotes, jobStatus — plus notes as a legacy alias for internalNotes.

Two differences from create to watch for: the status and type aliases are create-only, so on PUT send jobStatus and jobType; and clientId, jobNumber, targetObjectSlug and targetRecordId are not updatable — sending any of them returns 400 naming the key.

Status lifecycle

Job statuses are defined by your account's pipeline, not fixed by the API. A new account starts with draft, scheduled, in-progress, and completed, but pipelines are editable, so an established account often carries more — on-hold, invoiced, paid, cancelled and closed are all common.

Read the exact keys for your account from GET /api/v1/objects/job/schema, which returns every stage with its key and label in pipeline order. Filter and write using the key (in-progress), not the label (In Progress).

A typical lifecycle:

  1. draft — created from a request or quote, not yet on the dispatch board.
  2. scheduled — has a time window and at least one assignee.
  3. in-progress — a crew has started the first visit.
  4. completed — all visits are done and the job is ready to invoice.

For the user-facing equivalents, see the in-app overview of job and visit statuses.

Visits on jobs

A job has a time window (startDateTime / endDateTime), but creating it over the API does not generate a visit.

visits is not a field on this endpoint — sending it returns 400. And there is no POST /api/v1/visits, so the API cannot create visits at all. Visits are created in the app, including the series for multi-day and recurring work; read them with GET /api/v1/visits?jobId={jobId}, then shape each one with PATCH /api/v1/visits/{id}.

See Multi-day jobs overview and the Visits resource.

Typical patterns

  • Online booking sync: create with type: "one-off", status: "scheduled", assignedToTeams: [], and let the AI Dispatcher propose an assignee.
  • Recurring contracts: the recurrence rule is not part of this endpoint's contract — create the recurring job in the app (or ask us about the internal route) and FieldCamp expands the visit series. Over the API you can read and update the resulting job and its visits.
  • Quote-to-job: create with status: "draft", then PUT to status: "scheduled" once the client accepts. Line items are not part of a job over the API — they live on an estimate or invoice, so build the priced document there and keep the job as the scheduled work.
  • Field completion: when a tech marks a visit complete on mobile, the parent job moves to completed once the last visit is closed.

Endpoints

GET/api/v1/jobs

Authorization

BearerAuth
AuthorizationBearer <token>

Your FieldCamp API key (starts with fc_live_). Send it as Authorization: Bearer fc_live_… — or as the X-Api-Key: fc_live_… header. Create a key in Settings → API. This is an API key, not a login JWT.

In: header

Query Parameters

page?integer

Page number, 1-based.

limit?integer

Rows per page. Values above 100 are clamped to 100.

since?string

Return only rows whose updatedAt is at or after this ISO-8601 timestamp — the incremental-sync cursor. A value that is not a date returns 400 rather than being ignored. Include a timezone so the cutoff is unambiguous — UTC (2026-08-26T07:04:20Z) or an explicit offset (2026-08-26T07:04:20-06:00). A value without a timezone (e.g. 2026-08-26T07:04:20) is read as UTC; if you pass local wall-clock time, add the timezone parameter so it is converted correctly, otherwise the cutoff lands earlier than intended and pre-cutoff rows come through.

timezone?string

IANA timezone name (e.g. America/Denver) used to interpret a since that has no timezone of its own. When set, a naive since is read as local wall-clock time in this zone and converted to UTC. Ignored when since already carries a Z or an offset.

sort?string

Order the result as field:direction. Direction is asc or desc (default desc). Sortable fields: createdAt, updatedAt, startDateTime, endDateTime, jobNumber, total, jobStatus, priority. An unsupported field returns 400.

search?string

Free-text match on job number and the client's first name, last name or company name. Does not search addresses or line items.

clientId?string

Return only jobs for this client id.

status?string

Filter by job status. Matched case-insensitively against the whole value, so completed and Completed both work. Valid values are your account's own pipeline stage keys — read them from GET /api/v1/objects/job/schema; they are not a fixed global list.

jobStatus?string

Alias of status, kept for older integrations.

Response Body

application/json

application/json

application/json

curl -X GET "https://example.com/api/v1/jobs"
{}
POST/api/v1/jobs

Authorization

BearerAuth
AuthorizationBearer <token>

Your FieldCamp API key (starts with fc_live_). Send it as Authorization: Bearer fc_live_… — or as the X-Api-Key: fc_live_… header. Create a key in Settings → API. This is an API key, not a login JWT.

In: header

Request Body

application/json

Any key outside this list returns 400 with the offending key named — this API never accepts and silently discards a field.

TypeScript Definitions

Use the request body type in TypeScript.

Response Body

application/json

application/json

curl -X POST "https://example.com/api/v1/jobs" \  -H "Content-Type: application/json" \  -d '{    "clientId": "string"  }'
{}
GET/api/v1/jobs/{id}

Authorization

BearerAuth
AuthorizationBearer <token>

Your FieldCamp API key (starts with fc_live_). Send it as Authorization: Bearer fc_live_… — or as the X-Api-Key: fc_live_… header. Create a key in Settings → API. This is an API key, not a login JWT.

In: header

Path Parameters

id*string

Response Body

application/json

application/json

curl -X GET "https://example.com/api/v1/jobs/665f1c2ab3e4d5f6a7b8c9d0"
{}
PUT/api/v1/jobs/{id}

Authorization

BearerAuth
AuthorizationBearer <token>

Your FieldCamp API key (starts with fc_live_). Send it as Authorization: Bearer fc_live_… — or as the X-Api-Key: fc_live_… header. Create a key in Settings → API. This is an API key, not a login JWT.

In: header

Path Parameters

id*string

Request Body

application/json

Any key outside this list returns 400 with the offending key named — this API never accepts and silently discards a field.

TypeScript Definitions

Use the request body type in TypeScript.

Response Body

application/json

application/json

curl -X PUT "https://example.com/api/v1/jobs/665f1c2ab3e4d5f6a7b8c9d0" \  -H "Content-Type: application/json" \  -d '{}'
{}
DELETE/api/v1/jobs/{id}

Authorization

BearerAuth
AuthorizationBearer <token>

Your FieldCamp API key (starts with fc_live_). Send it as Authorization: Bearer fc_live_… — or as the X-Api-Key: fc_live_… header. Create a key in Settings → API. This is an API key, not a login JWT.

In: header

Path Parameters

id*string

Response Body

application/json

application/json

curl -X DELETE "https://example.com/api/v1/jobs/665f1c2ab3e4d5f6a7b8c9d0"
{}

Troubleshooting

400 jobNumber is required

You are sending multipart/form-data to the v1 endpoint. Switch to Content-Type: application/json and send the job object directly — no jobData wrapper.

A status filter returns fewer jobs than you expect

status matches the whole value case-insensitively, so casing is not the cause. Check the key against GET /api/v1/objects/job/schema — filtering on a label (In Progress) or on a stage your pipeline does not have returns an empty list, not an error.

Assignees are not showing on the calendar

Check that assignedToTeams contains user IDs, not team or role IDs. Get them from GET /api/teams — see the note at the top of this page.

Totals are wrong in the UI

Either send subTotal/discount/tax/total together, or omit all of them and let line-item pricing compute server-side. Mixing the two leads to mismatched values.

FAQs

Can I still call /api/jobs (no v1)? The unversioned route is deprecated. New integrations should target /api/v1/jobs. Existing callers should migrate before the next major release.

What is the difference between type: "one-off" and type: "recurring"? One-off jobs have a single time window. Recurring jobs carry a recurrence rule and FieldCamp expands them into a series of visits — see Multi-day jobs overview.

How do I do an incremental sync? Use GET /api/v1/jobs?since={iso_timestamp}&sort=updatedAt:asc on a schedule — the sort=updatedAt:asc matters, otherwise the default createdAt order combined with page/limit returns rows that look stale and you can't track a clean cursor. Save the last row's updatedAt as the next run's since. (Send since with a timezone — …Z or an offset — or add the timezone param.)

How do I filter by client? Pass clientId= on GET /api/v1/jobs. Combine with status and since to narrow the result further.

Where do I get user IDs for assignedToTeams? GET /api/teams returns the active users. Send your key on the Authorization header — see the Team resource for the response shape and the header note.

On this page