REST endpoints for workflows in ServFlo: 8 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}/workflows/book-job
What a CSR does on the phone, in one call: finds the customer (customerId, or by email, phone or exact name) or creates them, finds or creates the service property (propertyId, or by street address), picks the time (the given one, else the first open arrival window for the service from date, else 09:00) and books the job (JOB-n), optionally with a technician. Everything is checked before anything is written; each step is a normal API call in the activity log. Nothing is sent to the customer. Returns the job, the customer and property (and whether each was created), the slot and the job's url.
Permission
jobs.write (Owner, Admin, Dispatcher)
Risk
write, OAuth scope servflo.write. Write: creates or changes records inside the business; nothing leaves it.
An existing customer (GET /customers). Else give customer.
customer
object
No
Who the job is for when you have no customerId: matched to an existing customer by email, then phone, then exact name; created when none matches.
customer.name
string
Yes
Full name, or the company's name. Up to 500 characters.
customer.email
string (email)
No
Up to 254 characters.
customer.phone
string
No
Up to 50 characters.
customer.companyName
string
No
Up to 500 characters.
customer.type
enum
No
One of RESIDENTIAL, COMMERCIAL.
propertyId
string
No
An existing property of the customer. Else give address.
address
object
No
Where the work is, when you have no propertyId: matched to one of the customer's properties by street address; created (owned by the customer) when none matches.
address.address
string
Yes
Street address. Up to 500 characters.
address.address2
string
No
Up to 500 characters.
address.city
string
No
Up to 200 characters.
address.state
string
No
Up to 100 characters.
address.zip
string
No
Up to 20 characters.
serviceTypeId
string
No
What is done (GET /service-types): sets the price and length, and lets the first open arrival window be picked.
date
string
No
YYYY-MM-DD in the business's calendar. Default: the first day with an open window from today.
time
string
No
Hh: MM. Without it, the first open arrival window on or after date (needs serviceTypeId); else 09:00.
technicianId
string
No
Who goes (GET /technicians/options); unassigned without it.
notes
string
No
What the customer asked for. Up to 5000 characters.
priority
enum
No
One of LOW, NORMAL, HIGH, URGENT.
priceCents
integer
No
Overrides the service type's price (cents).
durationMinutes
integer
No
1–1440.
requestId
string
No
The service request this books (it becomes CONVERTED).
GET /organizations/{organizationId}/workflows/booking-slots
The step before book_job when the person wants to choose the time: the open arrival windows for a service (capacity.availability, from from, default today, for days days, default 7), with the booking request echoed back as request. Book a window with book_job, passing request plus the chosen date and time. In ChatGPT and Claude this shows a time picker the person books from. Takes the same customer, property and job details as book_job (only checked when booking). Read-only: nothing is created.
Permission
capacity.read (Owner, Admin, Dispatcher, Member)
Risk
read, OAuth scope servflo.read. Read: reads only.
MCP tool
find_booking_slots (toolset scheduling, in the default set)
An existing customer (GET /customers). Else give customer.
customer
query
object
No
Who the job is for when you have no customerId: matched to an existing customer by email, then phone, then exact name; created when none matches.
propertyId
query
string
No
An existing property of the customer. Else give address.
address
query
object
No
Where the work is, when you have no propertyId: matched to one of the customer's properties by street address; created (owned by the customer) when none matches.
serviceTypeId
query
string
Yes
What is done (GET /service-types): sets the length and which technicians can go.
technicianId
query
string
No
Who goes (GET /technicians/options); unassigned without it.
notes
query
string
No
What the customer asked for. Up to 5000 characters.
priority
query
enum
No
One of LOW, NORMAL, HIGH, URGENT.
priceCents
query
integer
No
Overrides the service type's price (cents).
durationMinutes
query
integer
No
1–1440.
requestId
query
string
No
The service request this books (it becomes CONVERTED).
POST /organizations/{organizationId}/workflows/quote-from-description
Turns a description of the work ("replace the 50-gallon water heater, haul the old one away") into a DRAFT quote (Q-n) for a customer: lines you give are used as they are; without them, each phrase of the description (split on commas, 'and', new lines) is matched to the pricebook (GET /pricebook/items search) and the best item becomes a line. Phrases with no match are listed in unmatched so you can add them (quotes.update) or ask the person. Nothing is sent: review it, then quotes.send. Returns the quote, the lines used, the unmatched phrases and the quote's url.
POST /organizations/{organizationId}/workflows/invoice-job-and-send
Customer-facing: invoices a job and sends the invoice in one step: reuses the job's open invoice or creates a DRAFT one from the job (service line and materials; jobs.createInvoice), then sends it by email (default) or text with the business's invoice message (messages.previewSend → messages.send), which issues it (SENT) and gives the customer the pay link. Consent applies (no text without OK-to-text). If sending is impossible (no address, consent) the invoice is still made and the answer says why. Returns the invoice, the message and the invoice's url.
POST /organizations/{organizationId}/workflows/reschedule-visit
Moves a job's visit to another day and time, like dragging it on the dispatch board: visitId, or the job's next open visit from jobId. Optionally hands it to another technician (technicianId; null leaves it unassigned) and changes its length. Returns the visit, scheduling conflicts for the technician (overlaps, outside working hours: warnings, the move is made) and the job's url. Nothing is sent to the customer by this call (automations may send their reschedule notice).
Permission
jobs.assign (Owner, Admin, Dispatcher)
Risk
write, OAuth scope servflo.write. Write: creates or changes records inside the business; nothing leaves it.
MCP tool
reschedule_visit (toolset scheduling, in the default set)
GET /organizations/{organizationId}/workflows/customer-overview
One customer at a glance: the record, the Customer 360 summary (lifetime value, health, behaviours, next service, predictions), and their open items: upcoming and open jobs, open quotes, unpaid invoices with the balance, active memberships and new requests. Give customerId, or query (name, email or phone) when it matches one customer. Parts your role cannot see are left out and named in hidden. Read-only.
GET /organizations/{organizationId}/workflows/whats-on-today
The day at a glance, from the dispatch board: each technician's visits in order (time, job, customer, service, address, status), the visits with nobody assigned, unscheduled work and new requests waiting, with totals. date defaults to today in the business's timezone. A technician sees only their own day. Read-only.
POST /organizations/{organizationId}/workflows/add-technician
Grants access: what "invite Sam as a technician" means in one step. Finds the technician record by email (else exact name) or creates it (technicians.create: the person jobs are assigned to), then gives them a ServFlo sign-in with the technician role linked to that record, so they see their own jobs and My day. For a technician who needs no sign-in, call technicians.create instead (no approval needed). An API key or AI app's call waits for the person's approval in ServFlo first (428 APPROVAL_REQUIRED); then the account is made at once and linked (team.invite, status ADDED; send them signInUrl). From the app it makes the Base316 invitation (technicians.invite; status INVITED, signInUrl is the acceptance link, emailed to them from ServFlo and returned once; invitationEmailed says whether the email went) and links them when they first sign in. Technicians take no office user. Returns the technician, what was made and the team screen's url.
Permission
members.manage (Owner, Admin)
Risk
sensitive, OAuth scope servflo.sensitive. Sensitive: moves money, deletes, sends or shows something to a customer, or grants or revokes access.
MCP tool
add_technician
Approval
Waits for your approval in ServFlo when called with an API key or a connected app: the first call answers 428 APPROVAL_REQUIRED; once you approve it, repeat the call with the X-ServFlo-Approval header. See Approvals.
Added: account made and linked now; INVITED: invitation made, linked at first sign-in; ALREADY_LINKED: the record already had a sign-in. One of ADDED, INVITED, ALREADY_LINKED.
userId
string or null
signInUrl
string or null
Where they sign in (ADDED) or accept the invitation (INVITED; returned once).
invitationEmailed
boolean or null
Invited: whether ServFlo emailed the acceptance link to them (else send signInUrl yourself). Null otherwise.
url
string
The ServFlo screen for the result (open it in the web app to see or change it).