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:
clientId— required. 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 asjobType. Defaults to"one-off".status— the pipeline stage key to start on, e.g."scheduled". Also accepted asjobStatus. Defaults to"draft"whenscheduleLateris true, otherwise"scheduled". The valid keys are your account's own — read them fromGET /api/v1/objects/job/schema.priority— string priority label used by the AI Dispatcher and dashboard filters.startDateTimeandendDateTime— UTC ISO-8601 timestamps.scheduleLater/anyTime— booleans.scheduleLatercreates the job without a time window;anyTimemarks 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 asnotes.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 to1.limit— rows per page. Defaults to30; values above100are not rejected — they are silently clamped to100(you still get200with 100 rows). Readmeta.totalfor the real count and page through the rest.sort— order the result asfield:direction, e.g.sort=total:descorsort=startDateTime:asc. Direction defaults todesc. Sortable fields:createdAt,updatedAt,startDateTime,endDateTime,jobNumber,total,jobStatus,priority. Any other field returns400.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 (completedandCompletedboth work). The valid values are your own pipeline's stage keys, not a fixed global list — read them fromGET /api/v1/objects/job/schema. Also accepted asjobStatusfor older integrations.since— return jobs whoseupdatedAtis at or after this ISO-8601 timestamp (the incremental-sync cursor). A value that is not a date returns400rather 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 thetimezoneparameter 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 asincethat has no timezone of its own. When set, a naivesinceis read as local wall-clock time in this zone and converted to UTC. Ignored whensincealready carries aZor 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.
PUT /api/v1/jobs/{id} with { startDateTime, endDateTime }.
Set jobStatus: "cancelled" via PUT — on update the field is jobStatus, not status. Prefer this over DELETE for auditability.
Set jobStatus: "completed" when the crew finishes. It also works the other way round: completing the last outstanding visit with PATCH /api/v1/visits/{id} rolls the job forward through your pipeline on its own.
Use GET /api/v1/jobs?search=<jobNumber> before re-creating.
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:
- draft — created from a request or quote, not yet on the dispatch board.
- scheduled — has a time window and at least one assignee.
- in-progress — a crew has started the first visit.
- 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", thenPUTtostatus: "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
completedonce the last visit is closed.
Endpoints
/api/v1/jobsAuthorization
BearerAuth 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 number, 1-based.
Rows per page. Values above 100 are clamped to 100.
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.
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.
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.
Free-text match on job number and the client's first name, last name or company name. Does not search addresses or line items.
Return only jobs for this client id.
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.
Alias of status, kept for older integrations.
Response Body
application/json
application/json
application/json
curl -X GET "https://example.com/api/v1/jobs"{}/api/v1/jobsAuthorization
BearerAuth 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" }'{}/api/v1/jobs/{id}Authorization
BearerAuth 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
Response Body
application/json
application/json
curl -X GET "https://example.com/api/v1/jobs/665f1c2ab3e4d5f6a7b8c9d0"{}/api/v1/jobs/{id}Authorization
BearerAuth 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
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 '{}'{}/api/v1/jobs/{id}Authorization
BearerAuth 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
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.
Related articles
Custom Objects | FieldCamp API
Read and write records on the custom objects your FieldCamp account defines — runtime schema discovery, cursor paging, delta sync, upsert by your own id, and object.* webhooks.
Visits | FieldCamp API
The FieldCamp Visits API schedules, updates, cancels, and tracks on-site appearances for jobs across an 8-state visit lifecycle.