Quickstart | FieldCamp API
FieldCamp API quickstart on v1 endpoints — go from an fc_live key to a scheduled job in five steps.
This FieldCamp API quickstart walks you from a fresh fc_live_ key to a scheduled job that shows up on the dispatch calendar — using the versioned /api/v1 routes shipped in release_v2. Every step is reversible: you can cancel the job, delete the demo client, and revoke the key once you're done experimenting. If you're brand new to the product, skim Quick start: set up your business in FieldCamp and the FieldCamp lead-to-payment workflow first so you know what each object represents in the UI. For the broader landscape of resources and tooling around the API, start at the API reference index.
All examples target the v1 API (/api/v1/...). The legacy unversioned
routes (/api/clients, /api/jobs, /api/team, /api/product-service,
/api/company-info) still work for older integrations but are frozen —
new builds should use /api/v1.
Before you start
You'll need three things before running any of the snippets below:
- A self-serve
fc_live_API key withclients:write,jobs:write, anditems:writescopes. Create one from Settings → Developer → API Keys following Authentication — and manage rotations through the API keys resource. - Node.js 20+ or Python 3.10+ if you want to run the end-to-end example at the bottom of this page.
- An admin or owner team role so you can confirm the created job in the calendar.
Set your key as an environment variable so the snippets pick it up automatically. The fc_live_ prefix is part of the secret — paste the whole thing:
export FIELDCAMP_API_KEY="fc_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx"Every v1 response follows the same envelope: { "success": true, "data": {...}, "meta": {...} } on success and { "success": false, "error": {...} } on failure. Check success before touching data — see Errors for the full shape.
Set up your HTTP client
Each step below assumes one of these clients is already initialized. Copy the relevant block once at the top of your file — every later step reuses it.
cURL
# No client setup — each cURL example is self-contained.
# $FIELDCAMP_API_KEY is the exported env var.
export FC_BASE="https://api.fieldcamp.ai/api/v1"Node.js
import axios from 'axios';
const fc = axios.create({
baseURL: 'https://api.fieldcamp.ai/api/v1',
headers: {
Authorization: `Bearer ${process.env.FIELDCAMP_API_KEY}`,
'Content-Type': 'application/json',
},
timeout: 30_000,
});Python
import os, requests
BASE = "https://api.fieldcamp.ai/api/v1"
key = os.environ["FIELDCAMP_API_KEY"]
HDRS = {
"Authorization": f"Bearer {key}",
"Content-Type": "application/json",
}Step 1 — Confirm your key works
Call GET /api/v1/company-info. A 200 with your company name proves the key, the tenant, and the network path are all healthy. The full schema lives in the Company Info resource.
Send the request
cURL
curl "$FC_BASE/company-info" \
-H "Authorization: Bearer $FIELDCAMP_API_KEY"Node.js
const { data } = await fc.get('/company-info');
if (!data.success) throw new Error(data.error?.message);
console.log(data.data.company.companyName);Python
r = requests.get(f"{BASE}/company-info", headers=HDRS).json()
assert r["success"], r.get("error")
print(r["data"]["company"]["companyName"])Verify the response envelope
Expected response shape:
{
"success": true,
"data": { "company": { "companyName": "Acme Moving Co.", "...": "..." } },
"meta": { "requestId": "req_01HV..." }
}If you see success: false or a 401, head to Authentication troubleshooting and confirm you're using an fc_live_ key — not a legacy JWT.
Step 2 — Create (or find) a client
A job needs a client. v1 supports server-side filtering, so look up by email before creating — that way retries are safe and you don't fill the CRM with duplicates. The full payload shape lives in the Clients resource.
cURL
# Look up first
curl "$FC_BASE/clients?email=jane@example.com" \
-H "Authorization: Bearer $FIELDCAMP_API_KEY"
# Create if missing
curl -X POST "$FC_BASE/clients" \
-H "Authorization: Bearer $FIELDCAMP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"firstName": "Jane",
"lastName": "Cooper",
"email": "jane@example.com",
"phoneNumber": { "countryCode": "+1", "number": "5550101234", "countryIdentifier": "us" },
"companyName": "Acme Moving Co.",
"propertyAddress": { "street": "500 Example Ave", "city": "Springfield", "state": "IL", "country": "United States", "zipCode": "62701" },
"billingAddress": { "street": "500 Example Ave", "city": "Springfield", "state": "IL", "country": "United States", "zipCode": "62701" },
"stage": "Active Client"
}'Node.js
const { data: lookup } = await fc.get('/clients', { params: { email: 'jane@example.com' } });
let clientId = lookup.data.clients?.[0]?.id;
if (!clientId) {
const { data: created } = await fc.post('/clients', {
firstName: 'Jane',
lastName: 'Cooper',
email: 'jane@example.com',
phoneNumber: { countryCode: '+1', number: '5550101234', countryIdentifier: 'us' },
companyName: 'Acme Moving Co.',
propertyAddress: { street: '500 Example Ave', city: 'Springfield', state: 'IL', country: 'United States', zipCode: '62701' },
billingAddress: { street: '500 Example Ave', city: 'Springfield', state: 'IL', country: 'United States', zipCode: '62701' },
stage: 'Active Client',
});
clientId = created.data.client.id;
}Python
lookup = requests.get(f"{BASE}/clients", headers=HDRS, params={"email": "jane@example.com"}).json()
client_id = (lookup["data"].get("clients") or [{}])[0].get("id")
if not client_id:
resp = requests.post(f"{BASE}/clients", headers=HDRS, json={
"firstName": "Jane", "lastName": "Cooper",
"email": "jane@example.com",
"phoneNumber": {"countryCode": "+1", "number": "5550101234", "countryIdentifier": "us"},
"companyName": "Acme Moving Co.",
"propertyAddress": {"street": "500 Example Ave", "city": "Springfield", "state": "IL", "country": "United States", "zipCode": "62701"},
"billingAddress": {"street": "500 Example Ave", "city": "Springfield", "state": "IL", "country": "United States", "zipCode": "62701"},
"stage": "Active Client",
}).json()
client_id = resp["data"]["client"]["id"]The new client appears immediately on the client detail page, and you can review its client categories and stages to confirm where Jane lands in your pipeline. Save the returned id — v1 normalizes everything to id, so the recordId / _id fallbacks from the legacy API are no longer required.
Step 3 — Create a product or service
Items are the billable products or services that appear as line items on a job. Create each item once in your price book and reuse its ID forever. For the API contract, see the Items resource; if you charge sales tax, the Taxes resource lists the codes you can attach to line items.
cURL
curl -X POST "$FC_BASE/items" \
-H "Authorization: Bearer $FIELDCAMP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Standard 3-person moving crew",
"description": "Up to 4 hours of on-site moving labor with 3 crew",
"price": 299.00,
"type": "Service",
"isActive": true
}'Node.js
const { data: itemResp } = await fc.post('/items', {
name: 'Standard 3-person moving crew',
description: 'Up to 4 hours of on-site moving labor with 3 crew',
price: 299.0,
type: 'Service',
isActive: true,
});
const itemId = itemResp.data.item.id;Python
resp = requests.post(f"{BASE}/items", headers=HDRS, json={
"name": "Standard 3-person moving crew",
"description": "Up to 4 hours of on-site moving labor with 3 crew",
"price": 299.00, "type": "Service", "isActive": True,
}).json()
item_id = resp["data"]["item"]["id"]Step 4 — Find a technician
A job's assignedToTeams field still takes user IDs from GET /api/v1/team, even though the field name says "teams". Use the v1 Team resource to grab one technician — see Adding and managing team members if your roster is empty.
cURL
curl "$FC_BASE/team?role=technician&limit=1" \
-H "Authorization: Bearer $FIELDCAMP_API_KEY"Node.js
const { data: teamResp } = await fc.get('/team', { params: { role: 'technician', limit: 1 } });
const technicianId = teamResp.data.team[0].id;Python
resp = requests.get(f"{BASE}/team", headers=HDRS, params={"role": "technician", "limit": 1}).json()
technician_id = resp["data"]["team"][0]["id"]Step 5 — Create the job
v1 standardizes POST /api/v1/jobs on JSON — no more multipart/form-data, no more jobData + notes form fields. Notes are now a first-class string on the payload. For the equivalent UI flow, see Creating a job.
If you're porting code from the legacy /api/jobs route, drop the
FormData wrapper, send Content-Type: application/json, and move notes
out of the form and into the JSON body. The v1 schema also enforces an
idempotent jobNumber — pick one that maps to your booking ID.
cURL
curl -X POST "$FC_BASE/jobs" \
-H "Authorization: Bearer $FIELDCAMP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"clientId": "'$CLIENT_ID'",
"jobNumber": "BOOKING-5512-MOVE",
"jobAddress": { "street": "500 Example Ave", "city": "Springfield", "state": "IL", "country": "United States", "zipCode": "62701" },
"jobPhone": { "countryCode": "+1", "number": "5550101234", "countryIdentifier": "us" },
"assignedToTeams": ["'$TECH_ID'"],
"jobType": "one-off",
"startDateTime": "2026-06-01T13:00:00.000Z",
"endDateTime": "2026-06-01T21:00:00.000Z",
"timezone": "America/Chicago",
"jobStatus": "scheduled",
"priority": "medium",
"subTotal": 299.00, "tax": 0.00, "total": 299.00,
"jobItems": [{
"itemId": "'$ITEM_ID'",
"itemName": "Standard 3-person moving crew",
"quantity": 1, "price": 299.00, "total": 299.00,
"type": "Service"
}],
"notes": "Booking 5512. Stairs: yes. Piano: no."
}'Node.js
const { data: jobResp } = await fc.post('/jobs', {
clientId,
jobNumber: 'BOOKING-5512-MOVE',
jobAddress: { street: '500 Example Ave', city: 'Springfield', state: 'IL', country: 'United States', zipCode: '62701' },
jobPhone: { countryCode: '+1', number: '5550101234', countryIdentifier: 'us' },
assignedToTeams: [technicianId],
jobType: 'one-off',
startDateTime: '2026-06-01T13:00:00.000Z',
endDateTime: '2026-06-01T21:00:00.000Z',
timezone: 'America/Chicago',
jobStatus: 'scheduled',
priority: 'medium',
subTotal: 299.0, tax: 0, total: 299.0,
jobItems: [{
itemId, itemName: 'Standard 3-person moving crew',
quantity: 1, price: 299.0, total: 299.0, type: 'Service',
}],
notes: 'Booking 5512. Stairs: yes. Piano: no.',
});
console.log('Created job:', jobResp.data.job.id);Python
resp = requests.post(f"{BASE}/jobs", headers=HDRS, json={
"clientId": client_id,
"jobNumber": "BOOKING-5512-MOVE",
"jobAddress": {"street": "500 Example Ave", "city": "Springfield", "state": "IL", "country": "United States", "zipCode": "62701"},
"jobPhone": {"countryCode": "+1", "number": "5550101234", "countryIdentifier": "us"},
"assignedToTeams": [technician_id],
"jobType": "one-off",
"startDateTime": "2026-06-01T13:00:00.000Z",
"endDateTime": "2026-06-01T21:00:00.000Z",
"timezone": "America/Chicago",
"jobStatus": "scheduled",
"priority": "medium",
"subTotal": 299.0, "tax": 0.0, "total": 299.0,
"jobItems": [{
"itemId": item_id, "itemName": "Standard 3-person moving crew",
"quantity": 1, "price": 299.0, "total": 299.0, "type": "Service",
}],
"notes": "Booking 5512.",
}).json()
print("Created job:", resp["data"]["job"]["id"])A visit is auto-generated from startDateTime/endDateTime. Refresh the calendar overview — the job is on it. If you need multiple stops, pass an empty visits: [] and create them explicitly with POST /api/v1/visits (see Visits and Understanding visits for what each status means).
Verify the job appears
v1 supports filtered list queries with cursor pagination, so you don't have to fetch every job to confirm yours landed.
curl "$FC_BASE/jobs?jobNumber=BOOKING-5512-MOVE" \
-H "Authorization: Bearer $FIELDCAMP_API_KEY"Response:
{
"success": true,
"data": {
"jobs": [
{ "id": "job_01HV...", "jobNumber": "BOOKING-5512-MOVE", "jobStatus": "scheduled" }
]
},
"meta": {
"pagination": { "cursor": null, "limit": 25, "total": 1 }
}
}Then open Job management in the UI to see the visit on the Job Tray and dispatch workspace and confirm everything looks right.
Full end-to-end Node.js example
Copy this into a single file (create-job.js), run with node create-job.js:
import axios from 'axios';
const KEY = process.env.FIELDCAMP_API_KEY;
if (!KEY) throw new Error('Set FIELDCAMP_API_KEY (fc_live_...)');
const fc = axios.create({
baseURL: 'https://api.fieldcamp.ai/api/v1',
headers: {
Authorization: `Bearer ${KEY}`,
'Content-Type': 'application/json',
},
timeout: 30_000,
});
async function run() {
// 1. Sanity-check the key.
const { data: who } = await fc.get('/company-info');
console.log('Authenticated as:', who.data.company.companyName);
// 2. Find or create the client (idempotent by email).
const email = `jane+${Date.now()}@example.com`;
const { data: lookup } = await fc.get('/clients', { params: { email } });
let clientId = lookup.data.clients?.[0]?.id;
if (!clientId) {
const { data: created } = await fc.post('/clients', {
firstName: 'Jane', lastName: 'Cooper', email,
phoneNumber: { countryCode: '+1', number: '5550101234', countryIdentifier: 'us' },
companyName: 'Acme Moving Co.',
propertyAddress: { street: '500 Example Ave', city: 'Springfield', state: 'IL', country: 'United States', zipCode: '62701' },
billingAddress: { street: '500 Example Ave', city: 'Springfield', state: 'IL', country: 'United States', zipCode: '62701' },
stage: 'Active Client',
});
clientId = created.data.client.id;
}
// 3. Create the billable item.
const { data: item } = await fc.post('/items', {
name: 'Standard 3-person moving crew',
description: 'Up to 4 hours of on-site moving labor with 3 crew',
price: 299.0, type: 'Service', isActive: true,
});
const itemId = item.data.item.id;
// 4. Pick a technician.
const { data: team } = await fc.get('/team', { params: { role: 'technician', limit: 1 } });
const technicianId = team.data.team[0].id;
// 5. Create the job (pure JSON — no FormData).
const { data: jobResp } = await fc.post('/jobs', {
clientId,
jobNumber: `DEMO-${Date.now()}`,
jobAddress: { street: '500 Example Ave', city: 'Springfield', state: 'IL', country: 'United States', zipCode: '62701' },
jobPhone: { countryCode: '+1', number: '5550101234', countryIdentifier: 'us' },
assignedToTeams: [technicianId],
jobType: 'one-off',
startDateTime: '2026-06-01T13:00:00.000Z',
endDateTime: '2026-06-01T21:00:00.000Z',
timezone: 'America/Chicago',
jobStatus: 'scheduled',
priority: 'medium',
subTotal: 299.0, tax: 0, total: 299.0,
jobItems: [{
itemId, itemName: 'Standard 3-person moving crew',
quantity: 1, price: 299.0, total: 299.0, type: 'Service',
}],
notes: 'Demo job created via Quickstart.',
});
console.log('Created job:', jobResp.data.job.id);
}
run().catch(err => { console.error(err.response?.data ?? err); process.exit(1); });Migrating from the legacy API
If you have an existing integration on the unversioned routes, here's the cheat sheet for moving to v1 — most projects can finish the swap in an afternoon.
| Legacy | release_v2 (v1) | Change |
|---|---|---|
POST /api/clients | POST /api/v1/clients | Same JSON body; response always exposes id. |
POST /api/product-service | POST /api/v1/items | Renamed for consistency with the price book. |
POST /api/jobs (multipart) | POST /api/v1/jobs (JSON) | notes moves into the JSON body, jobData wrapper removed. |
GET /api/clients (no filters) | GET /api/v1/clients?email= | Server-side filtering + cursor pagination via meta.pagination. |
X-API-Key header with JWT | Authorization: Bearer fc_live_… | Self-serve scoped keys; see Authentication. |
Troubleshooting
401 Unauthorized — Invalid API key
Your Authorization header is missing or malformed. Confirm there's exactly one space between Bearer and the secret, and that you're sending the full fc_live_… value — not just the trailing characters. Legacy JWTs still work but should be migrated; new keys created after release_v2 always carry the fc_live_ prefix.
400 Validation error — jobNumber is required
On v1 this almost always means a typo or a missing field in the JSON body. If you're porting from the legacy route, double-check you removed the FormData wrapper — multipart/form-data is no longer accepted at /api/v1/jobs. See the Jobs reference for the full schema.
409 Conflict — duplicate jobNumber
jobNumber is unique per tenant. Reuse the same value when retrying the same booking, and look up first with GET /api/v1/jobs?jobNumber=... before creating. See Idempotency for the safe-retry pattern.
The job doesn't show on the calendar
Three usual suspects: (1) assignedToTeams is empty, (2) startDateTime / endDateTime are in the past, or (3) the calendar view is filtered to a different team or week. Open Calendar views and clear filters.
429 Too Many Requests
You've hit the per-tenant rate limit. Back off using the Retry-After header — see Errors for the recommended retry policy.
FAQs
Do legacy /api routes still work?
Yes, the legacy /api/clients, /api/jobs, /api/team, /api/product-service, and /api/company-info routes remain available for backward compatibility. They're frozen — no new fields, no new resources — so any new integration should use /api/v1.
Is the response envelope the same on every v1 endpoint?
Yes. Every v1 response is { success, data, meta } on success or { success, error } on failure. List endpoints add meta.pagination with cursor, limit, and total. See Errors.
How do I rotate my fc_live key without downtime?
Create a second fc_live_ key with the same scopes, deploy it to your integration, then revoke the old one once you confirm traffic is on the new key. The Authentication guide covers automated rotation, and the API keys resource documents the programmatic endpoints.
Can the API create recurring jobs?
Not directly — create the first occurrence via the API, then use Workflow automation or the recurring jobs UI to generate the rest. The v1 schema may add a recurrence block in a future release; track changes in the API changelog.
What to build next
Create and rotate fc_live_ keys with the right scopes.
Full schema for /api/v1/jobs — reschedule, cancel, and list.
Subscribe to job, visit, and invoice events instead of polling.
Safe-retry patterns for jobs and clients in production pipelines.
Related articles
- API reference index
- API authentication
- API errors and retries
- Idempotency
- Rate limits
- Webhooks
- Webhook events catalog
- Jobs resource
- Clients resource
- Visits resource
- Items resource
- Team resource
- API keys resource
- API changelog
- Quick start: set up your business in FieldCamp
- Job management complete guide
- Calendar overview
- CRM overview
- Adding and managing team members
- Managing products and services
FieldCamp API Reference | FieldCamp
Build with the FieldCamp API reference — Swagger UI, OpenAPI spec, fc_live keys, JSON endpoints, webhooks, and resources for clients, jobs, and visits.
FieldCamp API Authentication: fc_live API Keys and Scopes
Set up FieldCamp API authentication with fc_live keys, X-Api-Key headers, and granular scopes for secure, role-based integrations.