JobField API.
Last updated 2026-10-17
The JobField API is what the JobField integration on Zapier uses to read and add records in a business's JobField account, and to hear about events as they happen. This page describes every endpoint the integration calls, how a connection is made, and what each request and event looks like.
Contents
- Overview
- Connecting a business
- Making requests
- Errors
- Rate limits
- Lists and pagination
- Repeated requests
- Instant triggers
- Endpoints
- Records
- Changes to this page
1. Overview
| What | Value |
|---|---|
| Base URL | https://my.jobfield.app/api/v1 |
| Format | JSON in and out, UTF-8 |
| Sign-in | OAuth 2.0 authorization code grant, with PKCE |
| Access token | Sent in the Authorization header on every request |
| Dates | ISO 8601 in UTC, for example 2026-09-24T14:00:00.000Z |
| Money | Dollars as a number. Totals and amounts also come in cents beside them. |
| Support | support@jobfield.app |
The API serves apps that JobField has registered. Today that is the JobField integration on Zapier. A business connects it by signing in to JobField and pressing Allow. To ask about access for another app, write to support@jobfield.app.
Each connection belongs to one business. Every request reads and writes only that business's records, and the business always comes from the access token. No request can name a different business.
A connection works while three things hold. The business's plan includes Zapier, the JobField account is not locked, and the person who pressed Allow is still an owner or an admin of the business.
What the Zapier integration calls
| Zapier step | Endpoint |
|---|---|
| Connection test and label | GET /me |
| New Booking Request | Instant trigger booking_request.created |
| New Customer | Instant trigger customer.created |
| New Job | Instant trigger job.created |
| Job Status Changed | Instant trigger job.status_changed |
| Job Completed | Instant trigger job.completed |
| Invoice Sent | Instant trigger invoice.sent |
| Payment Received | Instant trigger payment.received |
| New Call | Instant trigger call.completed |
| Turning a Zap on and off | POST /hooks and DELETE /hooks/{id} |
| Testing a trigger | GET /hooks/sample |
| Create Booking Request | POST /booking-requests |
| Create Customer | POST /customers |
| Create Job | POST /jobs |
| Find Customer | POST /customers/search |
| Find or Create Customer | POST /customers/search, then POST /customers when nothing is found |
| Find Job | GET /jobs?number= |
| Service, technician, arrival window and customer dropdowns | GET /service-types, GET /technicians, GET /booking-windows, GET /customers |
Two more endpoints are part of the API and are not called by the integration today: GET /booking-requests and GET /jobs with a status filter. They are described below as well.
2. Connecting a business
JobField runs its own OAuth 2.0 server. The integration uses the authorization code grant with a PKCE S256 challenge.
| Endpoint | Address |
|---|---|
| Allow screen | GET https://my.jobfield.app/oauth/authorize |
| Token | POST https://my.jobfield.app/api/oauth/token |
| Server metadata | GET https://my.jobfield.app/.well-known/oauth-authorization-server |
Lifetimes
| Credential | Starts with | Lasts |
|---|---|---|
| Authorization code | jfc_ |
10 minutes, and works once |
| Access token | jfat_ |
1 hour |
| Refresh token | jfrt_ |
Until the connection ends. It does not change when it is used. |
The integration needs no scopes. A connection can do what this page describes and nothing more. The server metadata lists scopes for other kinds of JobField connections, and they do not apply here.
Step 1. Send the person to the Allow screen
Open the Allow screen in the person's browser with these query parameters.
| Parameter | Required | Value |
|---|---|---|
response_type |
Yes | code |
client_id |
Yes | The app's ID, given by JobField when the app was registered. |
redirect_uri |
Yes | One of the return addresses registered for the app, matched exactly. |
state |
Yes | Any value up to 1,000 characters. It comes back unchanged. |
code_challenge |
Recommended | The PKCE S256 challenge, 43 to 128 characters of base64url. |
code_challenge_method |
No | S256, the only method accepted. When it is left out with a challenge, S256 is assumed. |
resource |
No | Leave it out. A value here is refused with invalid_target. |
The person signs in to JobField if they are not signed in already. The screen asks them to connect the app to their business and lists what the app can do. They then press Allow or Cancel.
The connection is made for the business JobField opens for that person, which is the first business they joined. Only an owner or an admin can press Allow, and only while the business's plan includes Zapier and the account is not locked. Anyone else sees a sentence that says why the connection cannot be made.
After Allow, the browser returns to redirect_uri with these parameters.
| Parameter | Value |
|---|---|
code |
The authorization code. |
state |
The value you sent. |
iss |
https://my.jobfield.app |
After Cancel, or when the request cannot go on, the browser returns to redirect_uri with error, state and iss. The error values are access_denied, invalid_request, invalid_target and unsupported_response_type. An unknown client_id, or a redirect_uri that is not registered for the app, is never sent back. The screen shows the error instead.
Step 2. Trade the code for tokens
Send a POST to the token endpoint. The body may be form-encoded or JSON. The app proves itself with its secret, either in the body or in an HTTP Basic Authorization header.
| Parameter | Value |
|---|---|
grant_type |
authorization_code |
code |
The code from step 1. |
redirect_uri |
The same address as in step 1. |
client_id |
The app's ID. |
client_secret |
The app's secret. |
code_verifier |
The PKCE verifier, required when step 1 sent a challenge. |
POST /api/oauth/token HTTP/1.1
Host: my.jobfield.app
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=jfc_...&redirect_uri=https%3A%2F%2Fzapier.com%2Fdashboard%2Fauth%2Foauth%2Freturn%2FApp246615CLIAPI%2F&client_id=zapier&client_secret=jfcs_...&code_verifier=...
{
"access_token": "jfat_...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "jfrt_..."
}
A code works once. If a code that was already used is presented again with the app's valid secret, JobField treats it as copied and ends the connection that was made with it. This holds for about a day after the code expires. After that the code is simply not valid.
Step 3. Refresh the access token
When an access token expires, send a POST to the same endpoint.
| Parameter | Value |
|---|---|
grant_type |
refresh_token |
refresh_token |
The refresh token from step 2. |
client_id |
The app's ID. |
client_secret |
The app's secret. |
{
"access_token": "jfat_...",
"token_type": "Bearer",
"expires_in": 3600
}
No new refresh token comes back. Keep the one you have and use it again next time. Refreshing twice at once is safe, and an access token issued earlier keeps working until its hour is over.
Token endpoint errors
Refusals use the OAuth format, {"error": "...", "error_description": "..."}, with the Cache-Control: no-store header.
| Status | error |
When |
|---|---|---|
| 400 | invalid_grant |
The code is not valid, was already used, has expired, or was issued for a different return address. The verifier does not match. The connection was removed. The person who pressed Allow is no longer an owner or an admin. The connection could not be saved, and the person needs to connect again. |
| 400 | invalid_target |
A resource parameter was sent. |
| 400 | unsupported_grant_type |
The grant type is not authorization_code or refresh_token. |
| 401 | invalid_client |
The app's ID or secret is wrong. |
| 429 | rate_limited |
Too many requests from one address. This answer uses the API error format in section 4 and carries a Retry-After header in seconds. |
| 500 | server_error |
The token could not be issued. Try again. |
A refresh that answers invalid_grant means the connection has ended, and the business needs to connect again.
When a connection ends
A connection has no end date. It ends when one of these happens.
- An owner or an admin presses Disconnect on the Zapier card in Settings → Integrations.
- The person who pressed Allow leaves the business, or their role drops below admin.
- A used authorization code is presented a second time.
From that moment every token of the connection stops working and its instant triggers are switched off. After Disconnect, and after a used code is presented again, JobField also sends a DELETE request to each subscribed hook address so that Zapier pauses those Zaps. When the connection ends because the person left or their role changed, the Zaps stay on in Zapier but receive no more events, and any Zap step that calls JobField fails until the business connects again.
The token revocation endpoint in the server metadata, /api/oauth/revoke, does not end connections made through the Zapier integration. When the app proves itself, it answers 200 for their tokens and changes nothing. Use Disconnect in JobField instead.
3. Making requests
Send the access token in the Authorization header on every request. JobField never reads a token from the address.
GET /api/v1/me HTTP/1.1
Host: my.jobfield.app
Authorization: Bearer jfat_...
Accept: application/json
- A request body must be a JSON object of up to 64 KB. An empty body counts as an empty object.
- Text values are trimmed, and a number is read as text. A value of any other type is treated as missing. A value longer than the field's limit is cut to the limit, except where an endpoint says otherwise, and the limits are listed with each endpoint.
- Booleans may be sent as
trueandfalse, as the numbers1and0, or as the text"true","yes","1"and"false","no","0"in lower case. Any other value counts as not sent. - Every answer carries
Cache-Control: no-store. - A create can take up to 30 seconds before it answers.
- JobField records every change made through the API as made by the connection, on behalf of the person who pressed Allow. New records show Zapier as their source. A lead from one of the four lead marketplaces shows the marketplace as its source, with Zapier after it.
Every request is checked in this order. The token exists and has not expired, its connection is still active, the person who connected it is still an owner or an admin, connecting apps is open for the business, the account is not locked, the plan includes Zapier, and the connection is under its rate limit.
4. Errors
Every error from /api/v1 has the same shape.
{
"error": {
"code": "validation_error",
"message": "The arrival window must be between 30 and 480 minutes.",
"field": "window_minutes"
}
}
The message is a sentence written for the business owner, and Zapier shows it as it is. The wording may change, so match on code. The field is present when one input caused the error.
| Status | code |
When | What to do |
|---|---|---|---|
| 400 | invalid_request |
The body is not a JSON object, is not valid JSON, could not be read, or is larger than 64 KB. | Fix the request. |
| 401 | invalid_token |
The token is missing, malformed, expired, or belongs to a connection that has ended. The answer carries WWW-Authenticate: Bearer error="invalid_token". |
Refresh the access token. If the refresh fails, the business connects again. |
| 403 | plan_required |
The business's plan does not include Zapier. | The owner changes the plan in JobField. |
| 403 | account_locked |
The JobField account is locked. | The owner signs in to JobField to see why. |
| 403 | forbidden |
The business has used its monthly allowance of jobs, on POST /jobs. It is also sent when connecting apps is not open for the business. |
For the job allowance, the owner changes the plan or waits for the next month. When connecting apps is not open, write to support@jobfield.app. |
| 422 | validation_error |
A value is missing or not valid. field names it. |
Fix the value. |
| 429 | rate_limited |
The connection is over its rate limit, or the same create is already running. The answer carries Retry-After in seconds. |
Wait that long and try again. |
| 500 | internal |
Something failed on JobField's side. | Try again. A create sent with an External ID or a Lead ID is safe to repeat. |
| 503 | unavailable |
JobField could not check the token for a moment. The answer carries Retry-After: 30. |
Wait and try again. The connection is not affected. |
A record that cannot be found is never a 404. A customer or technician named in a create that does not exist in the business is a 422, and a search with no match is an empty list. An address that is not an endpoint answers 404 with a web page instead of JSON, and a method an endpoint does not take answers 405.
5. Rate limits
| Limit | Value |
|---|---|
| Requests per connection | 120 a minute, counted in fixed one-minute windows |
| Requests to the token endpoint per address | 120 a minute |
| Refused guesses at the token endpoint per address | 30 a minute. After that, codes and tokens JobField never issued are refused with a 429 until the minute is over. |
| Active instant triggers per business | 200 |
Over a request limit, the answer is a 429 with Retry-After set to the seconds left in the current window, and Zapier waits that long and repeats the step. Past the limit of active instant triggers, POST /hooks answers a 422 on event.
6. Lists and pagination
A list answers with the records in data and a cursor for the next page in next_cursor.
{
"data": [{ "id": "6f1c2a10-3b4d-4e5f-8a9b-0c1d2e3f4a5b", "name": "Maria Alvarez" }],
"next_cursor": "WyIyMDI2LTA5LTIxVDE0OjA1OjAwLjAwMFoiLCI2ZjFjMmExMC0..."
}
- Records come newest first, by the time they were created.
limitsets the page size, from 1 to 100, and the default is 25. A larger value is read as 100.cursortakes thenext_cursorof the previous page. It is an opaque value, so do not build or change it. A cursor JobField cannot read is ignored, and the first page comes back.next_cursorisnullon the last page.
GET /customers, GET /booking-requests and GET /jobs page this way. The service, technician and arrival window lists, the searches and GET /hooks/sample answer in a single page, with next_cursor set to null.
7. Repeated requests
Zapier can run a step again, for example after a timeout or when a person replays a run. Creates are built so that a repeat returns the record the first run made instead of making a second one.
Booking requests: the Lead ID
POST /booking-requests uses lead_id, the ID the lead had where it came from.
- For a lead marketplace, the platforms
yelp,angi,thumbtackandgoogle_local_services, the Lead ID is required and can be up to 200 characters. The lead is filed under that marketplace, so the same lead never lands twice, including when JobField receives it from a direct connection to that marketplace. - For
facebook_lead_adsandgoogle_ads, and for every other platform whensource_nameis sent, the key is the platform, the source name and the Lead ID. A repeat with the same key is the same lead, even when other fields changed. Source names are compared without regard to letter case, spaces or punctuation. - For
website_form,email,otheror no platform, without asource_name, two form tools can share the same IDs. There the key is the Lead ID together with every other field of the request exceptplatform_reference, so only an exact repeat counts as the same lead. - Without a Lead ID, every request files a new booking request.
A repeat answers 200 with "duplicate": true and the booking request that was filed first. It fills in the name, phone number, email address, address and lead link when they were empty on that request, and adds the keys of platform_reference to the ones already kept, a new value replacing an old one under the same key. Every other field keeps what the first request sent.
Customers and jobs: the External ID
POST /customers and POST /jobs take an optional external_id, the record's ID in the other app.
- A repeat with the same External ID and the same fields, within 30 days, returns the record made the first time. The answer is a 200, with
"matched_existing": truefor a customer and"duplicate": truefor a job. - A request with the same External ID and different fields is treated as a new request. Two Zaps that happen to share an ID never return each other's records.
- While the first request is still running, a repeat is answered with a 429 and
Retry-After: 60. - If the first record was deleted, the next request makes a new one.
A customer is also not duplicated by phone or email. POST /customers returns the customer the business already has with the same email address, or the same phone number when it has at least ten digits, and POST /jobs books the job for that customer.
8. Instant triggers
Instant triggers use REST hooks. When a Zap turns on, Zapier subscribes to an event. JobField then sends each new event to the Zap's own address. When the Zap turns off, Zapier unsubscribes.
Events
| Event | Zapier trigger | Sent when | Record |
|---|---|---|---|
booking_request.created |
New Booking Request | A booking request or a message is added, from any source. | Booking request |
customer.created |
New Customer | A customer is added. Customers brought in by an import are not included. | Customer |
job.created |
New Job | A job is created. Jobs brought in by an import are not included. | Job |
job.status_changed |
Job Status Changed | A job moves from one status to another. Creating a job is not a change. | Job status change |
job.completed |
Job Completed | The work on a job is finished. See the rule below. | Job status change |
invoice.sent |
Invoice Sent | An invoice reaches the customer for the first time, by email or text, with or without a pay link. | Invoice |
payment.received |
Payment Received | A payment succeeds. A bank transfer counts once it has settled. | Payment |
call.completed |
New Call | A call ends, answered or missed. | Call |
A job counts as completed at each of these changes:
- It moves from Scheduled or In progress to Done.
- It moves from In progress to Invoiced, Not paid or Paid.
- It moves from Scheduled to Invoiced, Not paid or Paid, and the work was started or finished, or the job was due. A job is due when its start time is in the past or less than two hours away.
A deposit taken on a job booked for a later day is therefore not a completed job. A job that is reopened and finished again is sent again.
Subscribe
POST /hooks
| Field | Required | Value |
|---|---|---|
event |
Yes | One of the events above. |
target_url |
Yes | The Zap's hook address. It must use https on the host hooks.zapier.com, with no other port, no sign-in details in the address, and at most 2,000 characters. |
filters.status |
No | For job.status_changed only. Only changes into this status are sent: scheduled, in_progress, done, invoiced, not_paid, paid or cancelled. |
filters.direction |
No | For call.completed only. inbound or outbound. |
{
"event": "job.status_changed",
"target_url": "https://hooks.zapier.com/hooks/standard/123456/abcdef/",
"filters": { "status": "done" }
}
The answer is a 201 with the subscription's ID and its signing secret. The secret is shown this once. Keep it with the subscription to check every delivery.
{
"id": "0d6f3a2e-5b1c-4e8a-9f7d-2c4b6a8e0f13",
"event": "job.status_changed",
"secret": "jfwh_..."
}
A 422 names the field that is wrong: event, target_url, status or direction. A business can hold 200 active subscriptions, and past that the answer is a 422 on event.
Unsubscribe
DELETE /hooks/{id}
The answer is always a 200 with {"id": "...", "deleted": true}, including for a subscription that was already removed or never existed. Deliveries still waiting for that subscription are dropped. A business can only remove its own subscriptions.
Sample records
GET /hooks/sample?event=job.status_changed&status=done&limit=3
| Parameter | Value |
|---|---|
event |
Required. One of the events above. |
limit |
From 1 to 25. The default is 3. |
status |
Optional. For job.status_changed, the same filter as above. |
direction |
Optional. For call.completed, inbound or outbound. |
The answer lists recent records in exactly the shape a delivery has, as {"data": [...], "next_cursor": null}. The list can include imported customers and jobs, deleted jobs, and status changes that would not count as a completed job, so it is for testing, not for catching up on events. Zapier shows them when someone presses Test on a trigger. An unknown event is a 422 on event.
Deliveries
Each event is sent as an HTTP POST to the subscription's target_url.
| Header | Value |
|---|---|
Content-Type |
application/json |
User-Agent |
JobField-Webhooks/1.0 |
X-JobField-Event |
The event, for example payment.received. |
X-JobField-Event-Id |
The ID of this delivery. Retries of the same delivery carry the same ID. |
X-JobField-Signature |
The signature described below. |
The body is one record, the same shape as the sample records. It is built from the record as it is when the delivery is sent, so it shows the latest values. If the record was deleted before then, nothing is sent.
{
"id": "7e6d5c4b-3a29-4f18-8e07-d6c5b4a39281",
"amount": 265,
"amount_cents": 26500,
"tip": 20,
"surcharge": 0,
"method": "card",
"method_label": "Card",
"status": "succeeded",
"paid_at": "2026-09-24T13:40:00.000Z",
"job_id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"job_number": "JOB-01042",
"invoice_id": "4c3b2a19-8e7d-4c6b-9a5f-3e2d1c0b9a87",
"customer_id": "6f1c2a10-3b4d-4e5f-8a9b-0c1d2e3f4a5b",
"customer_name": "Maria Alvarez",
"customer_first_name": "Maria",
"customer_last_name": "Alvarez",
"customer_phone": "(305) 555-0142",
"customer_email": "maria.alvarez@example.com",
"url": "https://my.jobfield.app/crm/jobs/9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d"
}
- Deliveries go out every minute, so an event usually arrives within about a minute.
- The events of one subscription are sent one at a time. When an attempt fails, the rest of that round's events for the subscription wait for the next round, and the failed one waits for its retry. Events can therefore arrive out of order. When order matters, compare the time in the record, such as
changed_atorcreated_at. - Any 2xx answer counts as delivered. JobField waits up to 10 seconds for it and does not follow redirects.
- A delivery is tried up to 7 times. After a failed attempt JobField waits 1 minute, then 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours. After the seventh failure the delivery is marked as not delivered.
- An answer of 410 Gone switches the subscription off at once, and the deliveries still waiting for it are dropped.
- A subscription is also switched off after 20 failed attempts in a row with no successful delivery for 24 hours, so a short outage never turns a working Zap off.
- JobField does not tell Zapier when it switches a subscription off. The Zap stays on in Zapier and receives nothing until it is turned off and on again.
- Nothing is sent while the business's plan does not include Zapier or its account is locked. Those deliveries are skipped, not saved for later.
- A delivery can arrive twice, for example when the first attempt timed out after it was received. Use
X-JobField-Event-Idto tell a repeat from a new event.
The business owner sees the latest deliveries and their results on the Zapier card in Settings → Integrations.
Checking the signature
Every delivery is signed with the subscription's secret.
X-JobField-Signature: t=1790251200,v1=5f2b0c9e6a...
t is the time of this attempt in Unix seconds, and v1 is the hex HMAC-SHA256 of the text <t>.<body>, keyed with the whole secret as POST /hooks returned it, including jfwh_. To check a delivery:
- Read
tandv1from the header. - Compute the HMAC-SHA256 of
t, a full stop, and the raw body exactly as it arrived, before any parsing. - Compare the result with
v1in constant time. - Refuse the delivery when they differ, or when
tis more than 24 hours away from now.
const crypto = require("crypto");
function verifyJobFieldSignature(secret, rawBody, header, nowSeconds = Math.floor(Date.now() / 1000)) {
const parts = Object.fromEntries(
String(header || "").split(",").map((p) => {
const i = p.indexOf("=");
return [p.slice(0, i).trim(), p.slice(i + 1).trim()];
}),
);
const t = Number(parts.t);
if (!Number.isFinite(t) || !parts.v1) return false;
if (Math.abs(nowSeconds - t) > 24 * 3600) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`, "utf8").digest("hex");
const a = Buffer.from(expected, "utf8");
const b = Buffer.from(parts.v1, "utf8");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
The Zapier integration drops every delivery that does not carry a valid signature for its own subscription.
9. Endpoints
All paths are relative to https://my.jobfield.app/api/v1.
GET /me
Says which business and connection the token belongs to. Zapier calls it to test a connection and to name it.
{
"business": { "id": "94c9fa88-7e1d-4a39-a0c5-fc0b6bdaf353", "name": "Coastal Home Services" },
"connection": { "id": "3f2a1b0c-9d8e-4f7a-8b6c-5d4e3f2a1b0c" }
}
The business name is the one the business shows on its documents, or the name it was created with.
GET /service-types
The business's own services, for the Service dropdown. The id is what service takes in a create.
{ "data": [{ "id": "refrigerator", "name": "Refrigerator repair" }], "next_cursor": null }
GET /technicians
Everyone active on the business's team who can be assigned to a job, sorted by name, for the Technician dropdown. This can include owners and dispatchers. Only the ID and the name are returned.
{ "data": [{ "id": "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d", "name": "Luis Serrano" }], "next_cursor": null }
GET /booking-windows
The arrival windows a customer can ask for. The first entry is always Anytime. A window's id is its start time and its length in minutes. A business that has not set its own windows gets two-hour windows starting at 10:00, 12:00, 14:00, 16:00 and 18:00.
{
"data": [
{ "id": "anytime", "name": "Anytime" },
{ "id": "10:00-120", "name": "10am–12pm" }
],
"next_cursor": null
}
GET /customers
The business's customers, newest first, in pages. Deleted customers are left out. Each entry is a customer record, described in section 10.
| Parameter | Value |
|---|---|
limit |
From 1 to 100. The default is 25. |
cursor |
The next_cursor of the previous page. |
A phone or email parameter is refused with a 422, so that a filter is never mistaken for a match. Search with POST /customers/search instead.
POST /customers/search
Finds a customer by phone number or email address. It is a POST so that the phone number and email address travel in the body and never appear in an address or a request log.
| Field | Value |
|---|---|
phone |
Any format, up to 40 characters. The last ten digits are compared, and at least ten digits are needed. |
email |
Up to 254 characters. Compared without regard to letter case. |
Send at least one of the two, or the answer is a 422 on phone. The phone number is tried first, and the email address only when the phone finds nobody. The answer holds at most one customer, or none.
{ "phone": "305-555-0142" }
{ "data": [{ "id": "6f1c2a10-3b4d-4e5f-8a9b-0c1d2e3f4a5b", "name": "Maria Alvarez", "...": "..." }], "next_cursor": null }
POST /customers
Adds a customer. A customer the business already has with the same phone number or email address is returned as it is, and is never overwritten.
| Field | Limit | Notes |
|---|---|---|
first_name |
60 characters | first_name, last_name or name is required. |
last_name |
60 characters | |
name |
120 characters | The full name. When present, it is used instead of the first and last name. |
phone |
40 characters | Any format. |
email |
254 characters | An address that is not valid is left out, with a warning. |
address_line1 |
200 characters | |
city |
100 characters | |
state |
40 characters | |
zip |
20 characters | |
notes |
2,000 characters | |
external_id |
1,000 characters | See section 7. |
The answer is the customer record with two more fields. matched_existing is true when an existing customer was returned, and warnings lists the values that were left out. A new customer answers 201, and an existing one answers 200.
{
"id": "6f1c2a10-3b4d-4e5f-8a9b-0c1d2e3f4a5b",
"name": "Maria Alvarez",
"first_name": "Maria",
"last_name": "Alvarez",
"phone": "(305) 555-0142",
"phone_e164": "+13055550142",
"email": "maria.alvarez@example.com",
"address_line1": "1450 Brickell Ave",
"city": "Miami",
"state": "FL",
"zip": "33131",
"source": "zapier",
"source_label": "Zapier",
"created_at": "2026-09-21T14:05:00.000Z",
"url": "https://my.jobfield.app/crm/customers/6f1c2a10-3b4d-4e5f-8a9b-0c1d2e3f4a5b",
"matched_existing": false,
"warnings": []
}
GET /booking-requests
The business's booking requests, newest first, in pages, with the same limit and cursor as GET /customers.
POST /booking-requests
Adds a lead to the business's Bookings inbox, the same way a request from the business's own booking page arrives. It is attached to the customer the business already has with the same phone number or email address, and the owner is told about it.
| Field | Limit | Notes |
|---|---|---|
platform |
Where the lead came from: facebook_lead_ads, google_ads, website_form, email, yelp, angi, thumbtack, google_local_services or other. Any other value is a 422. |
|
source_name |
60 characters shown | Shown after the platform, for example a form's name. The whole value is used to tell leads apart. It is ignored for the four lead marketplaces. |
lead_id |
1,000 characters | The lead's ID on the platform. Required for the four lead marketplaces, and there up to 200 characters. Send it as text: a number too large to be exact is a 422. |
first_name |
60 characters | |
last_name |
60 characters | |
name |
120 characters | The full name, used instead of the first and last name when present. |
phone |
40 characters | Any format. |
email |
254 characters | An address that is not valid is left out, with a warning. |
address_line1 |
200 characters | |
city |
100 characters | |
state |
40 characters | |
zip |
20 characters | |
service |
120 characters | A service id from GET /service-types, or the customer's own words. |
issue |
4,000 characters | What the customer wrote. description is accepted as another name for it. |
preferred_date |
YYYY-MM-DD. A longer value that starts with a date keeps the date. Another format is left out with a warning, and a date that does not exist is left out without one. |
|
preferred_window |
A window id from GET /booking-windows, or anytime. A value JobField cannot read is left out, with a warning. |
|
urgent |
A boolean. | |
lead_url |
500 characters | A link to the lead on the platform. Only an https link with no sign-in details in it is kept. |
reply_email |
254 characters | A platform address for replying to the lead. It is kept with the lead and never saved as the customer's email. |
platform_reference |
4 KB | An object of other IDs from the platform to keep with the lead. A larger object is not kept. |
kind |
booking, the default, or message for a message that is not a request for a visit. |
A lead needs a way to reach the customer. Outside the four marketplaces, a request with no Lead ID, no phone number and no email address is a 422 on phone. A lead with a Lead ID but no phone number or email address is accepted when it carries an https lead link, a platform reference or a reply email. For website_form, email, other or no platform without a source_name, the Lead ID alone is enough, so map a phone number or an email address whenever the form has one.
A lead with no name is filed under its source's name followed by the word customer, for example Zapier customer. A repeat that carries a name replaces it.
The answer is the booking request record with three more fields. duplicate is true for a repeat, matched_existing_customer is true when the lead is attached to a customer, and warnings lists the values that were left out. A new booking request answers 201, and a repeat answers 200.
{
"id": "2b7e9c44-1d2e-4f3a-9b8c-7d6e5f4a3b2c",
"kind": "booking",
"status": "pending",
"name": "James Whitfield",
"first_name": "James",
"last_name": "Whitfield",
"phone": "(954) 555-0187",
"phone_e164": "+19545550187",
"email": "james.whitfield@example.com",
"address_line1": "88 SE 3rd Ave",
"city": "Fort Lauderdale",
"state": "FL",
"zip": "33301",
"service": "refrigerator",
"service_label": "Refrigerator repair",
"issue": "Fridge is not cooling, freezer is fine.",
"preferred_date": "2026-09-24",
"preferred_window": "10am–12pm",
"booked_for": null,
"urgent": false,
"source": "zapier",
"source_label": "Zapier · Website form",
"lead_id": "1234567890",
"customer_id": null,
"job_id": null,
"created_at": "2026-09-21T14:05:00.000Z",
"url": "https://my.jobfield.app/crm/bookings",
"duplicate": false,
"matched_existing_customer": false,
"warnings": []
}
GET /jobs
With number, finds one job by its number. With no number, lists the business's jobs, newest first, in pages. Deleted jobs are left out.
| Parameter | Value |
|---|---|
number |
The job number in any of its forms, for example 1042, #1042 or JOB-01042. Only the digits are read. |
status |
Optional, for the list. One job status: scheduled, in_progress, done, invoiced, not_paid, paid or cancelled. Any other value is a 422. |
limit |
For the list. From 1 to 100, and the default is 25. |
cursor |
For the list. The next_cursor of the previous page. |
A number search answers with at most one job. A number with no digits, or with more than nine, answers with an empty list.
POST /jobs
Creates a job with the status Scheduled. The customer is the one given by customer_id, or else the one the business already has with the same phone number or email address, or else a new customer made from the fields below. When a technician is given, the job is assigned to them and they get a notification. The job counts toward the business's monthly allowance of jobs like any other job.
| Field | Limit | Notes |
|---|---|---|
customer_id |
A customer ID in this business. An unknown ID is a 422. Either customer_id or a customer name is required. |
|
first_name |
60 characters | The customer's first name, used when no customer_id is given. |
last_name |
60 characters | |
name |
120 characters | The customer's full name. |
phone |
40 characters | The customer's phone number. |
email |
254 characters | The customer's email address. An address that is not valid is left out, with a warning. |
service |
120 characters | A service id from GET /service-types, or its name. Other words are kept as sent, and a job with no service gets other. |
issue |
4,000 characters | What needs doing. |
scheduled_at |
The start time as an ISO 8601 date and time with its offset, for example 2026-09-24T14:00:00-04:00. A time sent without an offset is read as UTC. Leave it out to create the job without a time. |
|
window_minutes |
30 to 480 | The arrival window. The default is 120. |
technician_id |
An active technician from GET /technicians. An unknown ID is a 422. |
|
address_line1 |
200 characters | The job's address. When the street, city and ZIP code are all left out, the customer's address is used. A new customer made by this request gets this address too. |
city |
100 characters | |
state |
40 characters | |
zip |
20 characters | |
notes |
4,000 characters | |
total |
0 to 1,000,000 | The job's total in dollars. The default is 0. |
external_id |
1,000 characters | See section 7. |
The answer is the job record with two more fields. duplicate is true for a repeat, and warnings lists the values this request left out. A repeat answers with an empty list. A new job answers 201, and a repeat answers 200. When the business has used its monthly allowance of jobs, the answer is a 403 forbidden and no job is made.
10. Records
The same fields come back from a create, a search, a list, a sample and a delivery. Keys are flat, a name comes whole and split into first and last name, an address comes in parts, and every record has a url that opens it in JobField. A value that is not known is null, except that names and the job's technicians are empty text, and a booking request with no window or service shows Anytime and other.
Customer
| Field | Value |
|---|---|
id |
The customer's ID. |
name, first_name, last_name |
The name, whole and split at the first space. |
phone |
The phone number as the business saved it. |
phone_e164 |
The number as +1 and ten digits when it has ten digits, or eleven starting with 1. Otherwise null. |
email |
The email address. |
address_line1, city, state, zip |
The address. |
source, source_label |
Where the customer came from, as a key and as JobField shows it. |
created_at |
When the customer was added. |
url |
The customer's page in JobField. |
Booking request
| Field | Value |
|---|---|
id |
The booking request's ID. |
kind |
booking or message. |
status |
pending, converted, dismissed or handled. |
name, first_name, last_name |
The name of the person who asked. |
phone, phone_e164, email |
How to reach them. phone can be the platform's relay number when that is all there is, and phone_e164 is set only for the customer's own number. |
address_line1, city, state, zip |
The address. |
service, service_label |
The service as it was given, and its name in the business's list when it has one. |
issue |
What the customer wrote. |
preferred_date |
YYYY-MM-DD, or null. |
preferred_window |
The window as JobField shows it, for example 10am–12pm, or Anytime. |
booked_for |
The time already booked for the visit, when there is one. |
urgent |
true or false. |
source, source_label |
Where the request came from, for example Zapier · Website form. |
lead_id |
The lead's ID on the platform it came from. |
customer_id |
The customer it is attached to. |
job_id |
The job it became, once it is booked. |
created_at |
When it arrived. |
url |
The Bookings inbox in JobField. |
Job
| Field | Value |
|---|---|
id |
The job's ID. |
job_number, number |
The number as an integer and as JobField prints it, for example 1042 and JOB-01042. |
status, status_label |
scheduled, in_progress, done, invoiced, not_paid, paid or cancelled, and its label, for example In progress. |
service, service_label |
The service. |
issue |
What needs doing. |
scheduled_at, window_minutes |
The start time and the arrival window in minutes. |
started_at, completed_at |
When the technician started and finished. |
total, total_cents |
The job's total in dollars and in cents. |
technician |
The main technician's name. |
technicians |
Every assigned technician, separated by commas. |
customer_id, customer_name, customer_first_name, customer_last_name |
The customer. |
customer_phone, customer_phone_e164, customer_email |
How to reach the customer. |
address_line1, city, state, zip |
The job's address. |
source, source_label |
Where the job came from. |
created_at |
When the job was created. |
url |
The job's page in JobField. |
Job status change
Sent for job.status_changed and job.completed. It holds every field of the job as it is now, and these fields describe the change.
| Field | Value |
|---|---|
id |
The job's ID and the change's number, joined by a colon. Each change has its own ID. |
job_id |
The job's ID. |
status, status_label |
The status the job moved to. |
previous_status, previous_status_label |
The status it moved from. |
changed_at |
When it moved. |
Invoice
| Field | Value |
|---|---|
id |
The invoice's ID. |
number |
The invoice number, the job number padded to five digits, for example 01042. |
status |
draft, sent, paid, refunded or void. |
amount, amount_cents |
The amount in dollars and in cents. |
sent_at, due_at, paid_at |
When it was sent, when it is due, and when it was paid. |
job_id, job_number |
The job it belongs to. Here job_number is the number as JobField prints it, for example JOB-01042. |
customer_id, customer_name, customer_first_name, customer_last_name, customer_phone, customer_email |
The customer. |
created_at |
When it was created. |
url |
The job's page in JobField. |
Payment
| Field | Value |
|---|---|
id |
The payment's ID. |
amount, amount_cents |
Everything the customer paid, in dollars and in cents. It includes the tip and the credit card surcharge. |
tip |
The tip in dollars. |
surcharge |
The credit card surcharge in dollars. |
method, method_label |
How the customer paid, as a key and as JobField shows it: card is Card, tap_to_pay is Tap to Pay, bank_transfer is Bank transfer, and cash, check, zelle, venmo and other keep their own names. Keys can be added, so show method_label to people. |
status |
succeeded. Only payments that succeeded are sent. |
paid_at |
When it was paid. |
job_id, job_number, invoice_id |
What it paid for. Here job_number is the number as JobField prints it, for example JOB-01042. |
customer_id, customer_name, customer_first_name, customer_last_name, customer_phone, customer_email |
The customer. |
url |
The job's page in JobField. |
Call
| Field | Value |
|---|---|
id |
The call's ID. |
direction |
inbound or outbound. |
status, status_label |
How the call ended: completed is Answered, no-answer and canceled are Missed, and busy, failed and voicemail keep their own names. |
phone, phone_e164 |
The other side's number. |
caller_name |
The caller's name as the phone network gave it. |
line_number |
The business number the call used. |
line_label, line_medium, line_campaign |
The label, medium and campaign the business set for that number. |
customer_id, customer_name, customer_email |
The matched customer. |
job_id |
The job linked to the call. |
started_at, ended_at, duration_seconds |
When it started, when it ended, and how long it lasted. |
answered_by |
Who picked up an incoming call: Receptionist, Team member or Voicemail, or null when nobody is recorded. Always null on an outgoing call. |
summary |
A short summary of the call, when there is one. |
url |
The call's page in JobField. |
11. Changes to this page
| Date | Change |
|---|---|
| 2026-10-17 | First published, describing version 1.0.1 of the Zapier integration. |