FieldCamp

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 with clients:write, jobs:write, and items:write scopes. 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.

Legacyrelease_v2 (v1)Change
POST /api/clientsPOST /api/v1/clientsSame JSON body; response always exposes id.
POST /api/product-servicePOST /api/v1/itemsRenamed 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 JWTAuthorization: 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

On this page