DevelopersAPI reference

Workflows API

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.

OperationEndpointRisk
Book a job (customer, property, time) in one stepPOST /organizations/{organizationId}/workflows/book-jobwrite
Find open times to book a jobGET /organizations/{organizationId}/workflows/booking-slotsread
Draft a quote from a description of the workPOST /organizations/{organizationId}/workflows/quote-from-descriptionwrite
Invoice a job and send it to the customerPOST /organizations/{organizationId}/workflows/invoice-job-and-sendsensitive
Reschedule a visit (and change who goes)POST /organizations/{organizationId}/workflows/reschedule-visitwrite
Everything about a customer (360 and open items)GET /organizations/{organizationId}/workflows/customer-overviewread
What's on today (the dispatch day)GET /organizations/{organizationId}/workflows/whats-on-todayread
Add a technician (schedule record and sign-in)POST /organizations/{organizationId}/workflows/add-techniciansensitive

Book a job (customer, property, time) in one step

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.

Permissionjobs.write (Owner, Admin, Dispatcher)
Riskwrite, OAuth scope servflo.write. Write: creates or changes records inside the business; nothing leaves it.
MCP toolbook_job (toolset scheduling, in the default set)

Request body

FieldTypeRequiredDescription
customerIdstringNoAn existing customer (GET /customers). Else give customer.
customerobjectNoWho 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.namestringYesFull name, or the company's name. Up to 500 characters.
customer.emailstring (email)NoUp to 254 characters.
customer.phonestringNoUp to 50 characters.
customer.companyNamestringNoUp to 500 characters.
customer.typeenumNoOne of RESIDENTIAL, COMMERCIAL.
propertyIdstringNoAn existing property of the customer. Else give address.
addressobjectNoWhere 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.addressstringYesStreet address. Up to 500 characters.
address.address2stringNoUp to 500 characters.
address.citystringNoUp to 200 characters.
address.statestringNoUp to 100 characters.
address.zipstringNoUp to 20 characters.
serviceTypeIdstringNoWhat is done (GET /service-types): sets the price and length, and lets the first open arrival window be picked.
datestringNoYYYY-MM-DD in the business's calendar. Default: the first day with an open window from today.
timestringNoHh: MM. Without it, the first open arrival window on or after date (needs serviceTypeId); else 09:00.
technicianIdstringNoWho goes (GET /technicians/options); unassigned without it.
notesstringNoWhat the customer asked for. Up to 5000 characters.
priorityenumNoOne of LOW, NORMAL, HIGH, URGENT.
priceCentsintegerNoOverrides the service type's price (cents).
durationMinutesintegerNo1–1440.
requestIdstringNoThe service request this books (it becomes CONVERTED).

Example

curl -X POST "https://app.servflo.ai/api/organizations/$ORGANIZATION_ID/workflows/book-job" \
  -H "Authorization: Bearer $SERVFLO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"customerId":"customer_id","customer":{"name":"Dana Whitfield"},"propertyId":"property_id"}'

Response

201: the answer's fields
FieldTypeDescription
jobobjectThe job (JOB-n), as POST /jobs returns it.
job.idstring
customerobjectThe customer (id, name …).
customer.idstring
customerCreatedboolean
propertyobject or nullThe service property, or null when none applies.
property.idstring
propertyCreatedboolean
slotobject
slot.datestringYYYY-MM-DD.
slot.timestring or nullHh: MM, or null for the default 09:00.
slot.sourceenumOne of GIVEN, FIRST_OPEN_WINDOW, DEFAULT.
urlstringThe ServFlo screen for the result (open it in the web app to see or change it).

Find open times to book a job

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.

Permissioncapacity.read (Owner, Admin, Dispatcher, Member)
Riskread, OAuth scope servflo.read. Read: reads only.
MCP toolfind_booking_slots (toolset scheduling, in the default set)

Parameters

NameInTypeRequiredDescription
customerIdquerystringNoAn existing customer (GET /customers). Else give customer.
customerqueryobjectNoWho 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.
propertyIdquerystringNoAn existing property of the customer. Else give address.
addressqueryobjectNoWhere 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.
serviceTypeIdquerystringYesWhat is done (GET /service-types): sets the length and which technicians can go.
technicianIdquerystringNoWho goes (GET /technicians/options); unassigned without it.
notesquerystringNoWhat the customer asked for. Up to 5000 characters.
priorityqueryenumNoOne of LOW, NORMAL, HIGH, URGENT.
priceCentsqueryintegerNoOverrides the service type's price (cents).
durationMinutesqueryintegerNo1–1440.
requestIdquerystringNoThe service request this books (it becomes CONVERTED).
fromquerystringNoFirst day to look at, YYYY-MM-DD. Default: today.
daysqueryintegerNoHow many days to show. 1–31. Default 7.

Example

curl "https://app.servflo.ai/api/organizations/$ORGANIZATION_ID/workflows/booking-slots?serviceTypeId=serviceType_id" \
  -H "Authorization: Bearer $SERVFLO_API_KEY"

Response

200: the answer's fields
FieldTypeDescription
requestobjectThe book_job arguments without date and time: add the chosen ones.
serviceTypeobject
serviceType.idstring
serviceType.namestring
customerobject or nullThe customer when customerId was given and may be read.
customer.idstring
customer.namestring
durationMinutesinteger
arrivalWindowMinutesinteger
daysarray of object
days[].datestring
days[].slotsarray of object
days[].slots[].timestring
days[].slots[].windowEndstring
days[].slots[].limitedboolean
days[].slots[].overThresholdboolean
urlstringThe ServFlo screen for the result (open it in the web app to see or change it).

Draft a quote from a description of the work

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.

Permission: quotes.write (Owner, Admin, Dispatcher, Technician (quotes they sold)); risk write (OAuth scope servflo.write).

Permissionquotes.write
Riskwrite, OAuth scope servflo.write. Write: creates or changes records inside the business; nothing leaves it.
MCP toolquote_from_description (toolset sales, in the default set)

Request body

FieldTypeRequiredDescription
customerIdstringYesThe customer (GET /customers).
propertyIdstringNo
requestIdstringNoThe service request this answers.
descriptionstringYesThe work, in plain words. Up to 5000 characters.
titlestringNoDefault: the description's first phrase. Up to 200 characters.
linesarray of objectNoUp to 100 items.
lines[].pricebookItemIdstringNoA pricebook item (GET /pricebook/items).
lines[].descriptionstringNoThe line's text (required without an item). Up to 1000 characters.
lines[].quantitynumberNo0.01–100000. Default 1.
lines[].unitPriceCentsintegerNoOverrides the item's price; required without an item.
validUntilstringNoA calendar date in the business's own calendar, YYYY-MM-DD.

Example

curl -X POST "https://app.servflo.ai/api/organizations/$ORGANIZATION_ID/workflows/quote-from-description" \
  -H "Authorization: Bearer $SERVFLO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"customerId":"customer_id","description":"string"}'

Response

201: the answer's fields
FieldTypeDescription
quoteobjectThe DRAFT quote (Q-n), as POST /quotes returns it.
quote.idstring
linesarray of object
lines[].descriptionstring
lines[].quantitynumber
lines[].unitPriceCentsinteger
lines[].pricebookItemIdstring or null
lines[].matchedFromstring or nullThe phrase it was matched from.
unmatchedarray of stringPhrases with no pricebook match: add them or ask.
urlstringThe ServFlo screen for the result (open it in the web app to see or change it).

Invoice a job and send it to the customer

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.

Permission: invoices.send (Owner, Admin, Dispatcher, Technician (own jobs' invoices)); risk sensitive (OAuth scope servflo.sensitive).

Permissioninvoices.send
Risksensitive, OAuth scope servflo.sensitive. Sensitive: moves money, deletes, sends or shows something to a customer, or grants or revokes access.
MCP toolinvoice_job_and_send (toolset invoices, in the default set)
ApprovalOver MCP, this tool asks for your OK first (in the AI app when it supports it, otherwise in ServFlo). See Approvals.

Request body

FieldTypeRequiredDescription
jobIdstringYesThe completed job to bill (GET /jobs).
channelenumNoOne of EMAIL, SMS. Default EMAIL.
messagestringNoReplaces the message body (default: the business's invoice template). Up to 5000 characters.

Example

curl -X POST "https://app.servflo.ai/api/organizations/$ORGANIZATION_ID/workflows/invoice-job-and-send" \
  -H "Authorization: Bearer $SERVFLO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"jobId":"job_id"}'

Response

200: the answer's fields
FieldTypeDescription
invoiceobjectThe invoice (INV-n).
invoice.idstring
invoiceCreatedboolean
sentboolean
messageobject or nullThe message as sent (status, channel, recipient).
notSentReasonstring or null
urlstringThe ServFlo screen for the result (open it in the web app to see or change it).

Reschedule a visit (and change who goes)

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).

Permissionjobs.assign (Owner, Admin, Dispatcher)
Riskwrite, OAuth scope servflo.write. Write: creates or changes records inside the business; nothing leaves it.
MCP toolreschedule_visit (toolset scheduling, in the default set)

Request body

FieldTypeRequiredDescription
jobIdstringYesThe job (GET /jobs).
visitIdstringNoThe visit (GET /jobs/{jobId}/visits). Default: the job's next open visit.
datestringYesThe new day, YYYY-MM-DD.
timestringYesThe new start, HH:MM.
technicianIdstring or nullNoHand the visit to this technician (null: unassigned). Omit to keep the crew.
durationMinutesintegerNo5–1440.

Example

curl -X POST "https://app.servflo.ai/api/organizations/$ORGANIZATION_ID/workflows/reschedule-visit" \
  -H "Authorization: Bearer $SERVFLO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"jobId":"job_id","date":"2026-11-03","time":"09:00"}'

Response

200: the answer's fields
FieldTypeDescription
visitobject
visit.idstring
conflictsarray of objectWarnings for the technician's day; the move is made.
urlstringThe ServFlo screen for the result (open it in the web app to see or change it).

Everything about a customer (360 and open items)

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.

Permissioncustomers.read (Owner, Admin, Dispatcher, Technician, Member)
Riskread, OAuth scope servflo.read. Read: reads only.
MCP toolcustomer_overview (toolset customers, in the default set)

Parameters

NameInTypeRequiredDescription
customerIdquerystringNo
queryquerystringNoName, email or phone. Up to 200 characters.

Example

curl "https://app.servflo.ai/api/organizations/$ORGANIZATION_ID/workflows/customer-overview" \
  -H "Authorization: Bearer $SERVFLO_API_KEY"

Response

200: the answer's fields
FieldTypeDescription
customerobject
customer.idstring
insightobject or nullCustomer 360 (GET /customers/{customerId}/insights).
openJobsarray of object
openQuotesarray of object
unpaidInvoicesarray of object
balanceDueCentsintegerSum of the unpaid invoices' balances, in cents.
membershipsarray of object
openRequestsarray of object
hiddenarray of stringSections left out because the role cannot read them.
urlstringThe ServFlo screen for the result (open it in the web app to see or change it).

What's on today (the dispatch day)

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.

Permissionschedule.read (Owner, Admin, Dispatcher, Technician, Member)
Riskread, OAuth scope servflo.read. Read: reads only.
MCP toolwhats_on_today (toolset scheduling, in the default set)

Parameters

NameInTypeRequiredDescription
datequerystringNoA calendar date in the business's own calendar, YYYY-MM-DD.

Example

curl "https://app.servflo.ai/api/organizations/$ORGANIZATION_ID/workflows/whats-on-today" \
  -H "Authorization: Bearer $SERVFLO_API_KEY"

Response

200: the answer's fields
FieldTypeDescription
datestring
totalsobject
totals.visitsinteger
totals.completedinteger
totals.inProgressinteger
totals.unassignedinteger
totals.unscheduledinteger
totals.newRequestsinteger
techniciansarray of object
technicians[].idstring
technicians[].namestring
technicians[].visitsarray of object
unassignedarray of object
unscheduledarray of object
requestsarray of object
urlstringThe ServFlo screen for the result (open it in the web app to see or change it).

Add a technician (schedule record and sign-in)

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.

Permissionmembers.manage (Owner, Admin)
Risksensitive, OAuth scope servflo.sensitive. Sensitive: moves money, deletes, sends or shows something to a customer, or grants or revokes access.
MCP tooladd_technician
ApprovalWaits 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.

Request body

FieldTypeRequiredDescription
namestringYesTheir full name. Up to 200 characters.
emailstring (email)YesTheir email: their sign-in, and how an existing record is found. Up to 254 characters.
phonestringNoUp to 50 characters.
workDaysarray of integerNoDays they work, 0 = Sunday … 6 = Saturday (default Monday–Friday). Up to 7 items.

Example

curl -X POST "https://app.servflo.ai/api/organizations/$ORGANIZATION_ID/workflows/add-technician" \
  -H "Authorization: Bearer $SERVFLO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Dana Whitfield","email":"dana@example.com"}'

Response

200: the answer's fields
FieldTypeDescription
technicianobjectThe technician record (jobs are assigned to it).
technician.idstring
technicianCreatedboolean
accessenumAdded: 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.
userIdstring or null
signInUrlstring or nullWhere they sign in (ADDED) or accept the invitation (INVITED; returned once).
invitationEmailedboolean or nullInvited: whether ServFlo emailed the acceptance link to them (else send signInUrl yourself). Null otherwise.
urlstringThe ServFlo screen for the result (open it in the web app to see or change it).

On this page