REST endpoints for field in ServFlo: 13 operations with parameters, request and response fields, permissions and curl examples.
Base URL: https://app.servflo.ai/api. Every call needs Authorization: Bearer sfk_… (Authentication); {organizationId} ($ORGANIZATION_ID in the examples) is your business's id.
POST /organizations/{organizationId}/jobs/{jobId}/on-my-way
Marks a SCHEDULED or CONFIRMED job 'On the way' (onMyWayAt, visible to the office) and starts the technician's TRAVEL clock (stopping any other travel). Emits technician.on_my_way: SENDS TO THE CUSTOMER when the business has the "On my way" automation on (a text with the technician's name and an ETA). Technicians only for their own jobs (404 otherwise).
Permission
jobs.work (Owner, Admin, Dispatcher, Technician)
Risk
write, OAuth scope servflo.write. Write: creates or changes records inside the business; nothing leaves it.
POST /organizations/{organizationId}/jobs/{jobId}/arrive
Stamps arrivedAt and stops the technician's TRAVEL clock for this job. The job is not started; call POST …/start for that (starting also counts as arriving).
Permission
jobs.work (Owner, Admin, Dispatcher, Technician)
Risk
write, OAuth scope servflo.write. Write: creates or changes records inside the business; nothing leaves it.
POST /organizations/{organizationId}/jobs/{jobId}/start
Sets the job IN_PROGRESS with startedAt, stops the technician's travel and any other running job clock, and starts a JOB time entry for this job. Emits job.started. Nothing is sent to the customer.
Permission
jobs.work (Owner, Admin, Dispatcher, Technician)
Risk
write, OAuth scope servflo.write. Write: creates or changes records inside the business; nothing leaves it.
POST /organizations/{organizationId}/jobs/{jobId}/complete
Sets the job COMPLETED with completedAt and optional completion notes, stops its JOB and TRAVEL time, submits its complete draft forms and books a recurring job's next visit (nextJobId). Refused with 400 naming the missing answers while any form on the job lacks a required field: fill them with PATCH …/forms/{formId} first. Take the customer's signature with POST …/signature. Emits job.completed.
Permission
jobs.work (Owner, Admin, Dispatcher, Technician)
Risk
write, OAuth scope servflo.write. Write: creates or changes records inside the business; nothing leaves it.
POST /organizations/{organizationId}/jobs/{jobId}/signature
Stores the customer's sign-off: a PNG drawn on the device (base64 or a data URL, at most 300 KB) as a SIGNATURE file on the job, with who signed, when, and the signing device's address and browser. Shown on the job and as 'Work accepted by' on invoices made from it. Stores the statement the customer agreed to (terms, else the business's current sign-off statement) and the work and amounts shown (summary). Signing again replaces the previous signature. Emits job.signed.
Permission
jobs.work (Owner, Admin, Dispatcher, Technician)
Risk
write, OAuth scope servflo.write. Write: creates or changes records inside the business; nothing leaves it.
GET /organizations/{organizationId}/jobs/{jobId}/photos
Confirmed photos on the job, newest first, with tag (BEFORE, AFTER, GENERAL, ISSUE, or SIGNATURE for the customer's sign-off) and view/download URLs valid for 10 minutes. Read-only. Technicians: their own jobs only.
POST /organizations/{organizationId}/jobs/{jobId}/photos
Step 1 of 2 of adding a photo to a job: records it with its tag and returns uploadUrl; PUT the image there (same Content-Type, within 5 minutes), then call POST …/photos/{attachmentId}/confirm. Images up to 25 MB (the app downscales to 2000 px first), or short videos (video/mp4, video/quicktime) up to 150 MB. Unconfirmed uploads are not listed.
Permission
jobs.work (Owner, Admin, Dispatcher, Technician)
Risk
write, OAuth scope servflo.write. Write: creates or changes records inside the business; nothing leaves it.
POST /organizations/{organizationId}/jobs/{jobId}/photos/{attachmentId}/confirm
Step 2 of 2: marks the photo as uploaded so it is listed on the job (and in the portal, when the business shows job photos there and the job is completed). Returns the photo with its URLs.
Permission
jobs.work (Owner, Admin, Dispatcher, Technician)
Risk
write, OAuth scope servflo.write. Write: creates or changes records inside the business; nothing leaves it.
POST /organizations/{organizationId}/jobs/{jobId}/photos/content
Adds a photo (or a short video/mp4 or video/quicktime) to a job in one call, tagged BEFORE, AFTER, GENERAL or ISSUE: send contentBase64 or a public sourceUrl (up to 10 MB). It is listed on the job (and in the customer portal when the business shows completed jobs' photos). Technicians: their own jobs. Returns the photo with 10-minute URLs.
Permission
jobs.work (Owner, Admin, Dispatcher, Technician)
Risk
write, OAuth scope servflo.write. Write: creates or changes records inside the business; nothing leaves it.
MCP tool
jobPhotos_upload (toolset scheduling, in the default set)
BEFORE, AFTER, GENERAL or ISSUE (what the photo shows). One of BEFORE, AFTER, GENERAL, ISSUE. Default GENERAL.
fileName
string
No
The file's name, e.g. receipt.jpg (default: from the URL, else 'file'). Up to 500 characters.
contentType
string
No
MIME type, e.g. image/jpeg or application/pdf. Optional: taken from a data: URL, the URL's answer or the file's first bytes. Up to 200 characters.
contentBase64
string
No
The file's bytes as base64 (a data: URL is accepted too). Up to 10 MB. Give this or sourceUrl.
sourceUrl
string (uri)
No
A public http(s) URL ServFlo downloads the file from (up to 10 MB, 20 s, 3 redirects; private and internal addresses are refused). Give this or contentBase64. Up to 2000 characters.
The visits on date for a technician (every visit whose crew they are on, cancelled left out), in the dispatcher's route order when set, else by time, each with customer and phone, address, access notes, gate code and hazards, equipment, forms still to fill and the straight-line distance from the previous stop when both are geocoded; plus the technician's clock (clocked in, on a break, today's paid minutes). A technician always gets their own day; others pass technicianId (GET /technicians/options) or get their own linked technician's day. technician: null means the caller is not linked to a technician record yet. Read-only.
The day, YYYY-MM-DD. Defaults to today in the organization's timezone.
technicianId
query
string
No
Whose day (office roles); ignored for technicians.
timeZone
query
string
No
IANA timezone days and weeks are counted in (e.g. America/Chicago). Defaults to the organization's timezone (Settings → General). Up to 100 characters.
Whether the technician has a ServFlo account linked.
visits
array of object
visits[].id
string
The job's id (as before visits existed).
visits[].jobId
string
visits[].visitId
string
The visit: use it with POST /jobs/{jobId}/visits/{visitId}/…
visits[].identifier
string
visits[].status
enum
The visit's status. One of SCHEDULED, CONFIRMED, IN_PROGRESS, COMPLETED, CANCELLED.
visits[].jobStatus
enum
One of SCHEDULED, CONFIRMED, IN_PROGRESS, COMPLETED, CANCELLED.
visits[].routeOrder
integer or null
The dispatcher's route position; the day is in this order when set.
visits[].visitNotes
string or null
Notes for this visit (the job's are notes).
visits[].crew
array of object
visits[].crew[].technicianId
string
visits[].crew[].isLead
boolean
visits[].crew[].name
string
visits[].crew[].color
string
visits[].priority
enum
One of LOW, NORMAL, HIGH, URGENT.
visits[].scheduledDate
string
visits[].scheduledTime
string or null
visits[].durationMinutes
integer
visits[].address
string or null
visits[].notes
string or null
visits[].completionNotes
string or null
visits[].onMyWayAt
string (date-time) or null
visits[].arrivedAt
string (date-time) or null
visits[].startedAt
string (date-time) or null
visits[].completedAt
string (date-time) or null
visits[].signedBy
string or null
visits[].signedAt
string (date-time) or null
visits[].customer
object
visits[].customer.id
string
visits[].customer.name
string
visits[].customer.phone
string or null
visits[].customer.email
string or null
visits[].customer.doNotService
boolean
visits[].customer.doNotServiceReason
string or null
visits[].property
object or null
visits[].property.id
string
visits[].property.name
string or null
visits[].property.address
string or null
visits[].property.address2
string or null
visits[].property.city
string or null
visits[].property.state
string or null
visits[].property.zip
string or null
visits[].property.gateCode
string or null
visits[].property.accessInstructions
string or null
visits[].property.safetyNotes
string or null
visits[].property.petsOnSite
boolean
visits[].property.petNotes
string or null
visits[].property.contactName
string or null
visits[].property.contactPhone
string or null
visits[].property.latitude
number or null
visits[].property.longitude
number or null
visits[].asset
object or null
visits[].asset.id
string
visits[].asset.name
string
visits[].asset.serialNumber
string
visits[].asset.manufacturer
string or null
visits[].asset.model
string or null
visits[].serviceType
object or null
visits[].serviceType.id
string
visits[].serviceType.name
string
visits[].serviceType.color
string
visits[].distanceFromPreviousKm
number or null
Straight-line km from the previous stop, when both are geocoded.
visits[].membership
object or null
Set when the customer is a member: plan name and whether their work is priority.
visits[].membership.membershipId
string
visits[].membership.identifier
string
MEM-n
visits[].membership.planName
string
visits[].membership.priority
boolean
The plan flags its members' work as priority on the board.
visits[].membership.dueMonth
string or null
YYYY-MM when the job is an included member visit; null for other work for a member.
visits[].membership.dueDate
string or null
YYYY-MM-DD when the member visit is on a week cadence (every N weeks); null for month-based visits.
visits[].photoCount
integer
visits[].forms
array of object
visits[].forms[].id
string
visits[].forms[].name
string
visits[].forms[].status
enum
One of DRAFT, SUBMITTED.
visits[].forms[].missingCount
integer
clock
object or null
clock.clockedIn
boolean
clock.onBreak
boolean
clock.activity
enum or null
The non-job time running now, if any. One of SHOP, SUPPLY_HOUSE, MEETING, TRAINING, VEHICLE, ADMIN, OTHER.
clock.running
array of object
clock.running[].id
string
clock.running[].organizationId
string
clock.running[].userId
string
clock.running[].technicianId
string or null
clock.running[].jobId
string or null
clock.running[].kind
enum
SHIFT (clocked in), JOB (working a job), TRAVEL (on the way), BREAK, ACTIVITY (non-job work: see activity). One of SHIFT, JOB, TRAVEL, BREAK, ACTIVITY.
clock.running[].activity
enum or null
What an ACTIVITY entry was spent on. One of SHOP, SUPPLY_HOUSE, MEETING, TRAINING, VEHICLE, ADMIN, OTHER.
clock.running[].startedAt
string (date-time)
clock.running[].endedAt
string (date-time) or null
clock.running[].note
string or null
clock.running[].source
enum
One of CLOCK, MANUAL, AUTO.
clock.running[].status
enum
Approval state: OPEN, SUBMITTED, APPROVED or REJECTED. Running = endedAt null. One of OPEN, SUBMITTED, APPROVED, REJECTED.
clock.running[].editReason
string or null
clock.running[].editedByUserId
string or null
clock.running[].approvedByUserId
string or null
clock.running[].approvedAt
string (date-time) or null
clock.running[].createdAt
string (date-time)
clock.running[].updatedAt
string (date-time)
clock.running[].job
object or null
clock.running[].technician
object or null
clock.running[].userName
string or null
clock.running[].minutes
integer
Whole minutes (a running entry counts to now).
clock.running[].date
string
The day it started, in the requested timezone.
clock.today
object
clock.today.SHIFT
integer
clock.today.JOB
integer
clock.today.TRAVEL
integer
clock.today.BREAK
integer
clock.today.ACTIVITY
integer
clock.today.paid
integer
Paid minutes: SHIFT minus BREAK on a day with a shift, else JOB plus TRAVEL plus ACTIVITY.
Every active technician with whether they are clocked in (and since when) or on a break, the job they are on (IN_PROGRESS, else ON_THE_WAY), their next job, progress through date's jobs and paid minutes so far. For the dashboard's Team today card. A technician sees only their own row. Read-only.
The day, YYYY-MM-DD. Defaults to today in the organization's timezone.
timeZone
query
string
No
IANA timezone days and weeks are counted in (e.g. America/Chicago). Defaults to the organization's timezone (Settings → General). Up to 100 characters.