Jobs, schedules, and expenses
Jobs, schedules, and expenses in the BlueSuite REST API
Jobs group customer work, visits, and billing. See Events for visits and Payments for recording money received.
Status and holds
Scheduled and in-progress states follow appointments. Manual status updates accept completed, cancelled, or reopen; both the general update and dedicated status endpoint use the guarded status transition. Required forms can prevent completion.
Use POST /jobs/:id/hold for a temporary hold, with an explicit cancel_appointments decision. Resume with POST /jobs/:id/resume, passing the current held_at value as expected_held_at; this prevents acting on a hold that has changed. A recurring job may need a resume_on date. Holds are separate from ordinary status changes.
Recurring schedules
Creating a job with type: "recurring" alone does not set up its visits. Use POST /jobs/:id/schedule to establish the schedule and billing settings together. Dates and times use the workspace timezone. Both times are required unless all_day: true. For custom weekly/monthly schedules, provide repeat_frequency_schedule.days; only send that schedule with repeat_frequency_unit: "custom". A finite duration requires repeat_duration.
billing_type defaults to per_visit, which generates invoices as visits are completed. Use manual when billing will be handled separately. invoice_autopay is a job field; the schedule endpoint takes payment_terms, while ordinary job create/update takes invoice_payment_terms.
To stop repeating work, use POST /jobs/:id/schedule/end with the last date to retain. It removes later scheduled occurrences while preserving completed/invoiced history. Do not delete the job to stop its schedule.
curl 'https://my.bluesuite.app/api/v1/jobs/42/schedule' \
-H "Authorization: Bearer $BLUESUITE_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"start_date":"2026-11-02","start_time":"09:00","end_time":"10:00","repeat_frequency_unit":"weekly","repeat_duration_unit":"forever","billing_type":"manual"}'Expenses
Expense endpoints are nested under the parent job. Amounts may be positive, zero, or negative, so credits can be recorded. Use Uploads to attach receipts to the job. Expense creation does not itself upload a file.
List jobs using comma-separated statuses or tag_ids to match any selected tag. See line items for pricing.
GET /api/v1/jobs
GET /api/v1/jobsRequired scopes: jobs:read.
Query parameters
| Field | Type / accepted values | Required | Details |
|---|---|---|---|
page | integer | No | Page number, starting at 1. Default: 1. |
per_page | integer | No | Records per page, from 1 to 100. Default: 25. minimum: 1. maximum: 100. |
search | string | No | Search text. maxLength: 100. |
sort_by | string | No | Sort column. See the supported columns below when listed. |
sort_direction | "asc", "desc" | No | Sort direction. Default: "desc". |
contact_id | string | No | Filter by contact ID. |
quote_id | string | No | Filter by quote ID. |
status | string | No | Filter by status. |
derived_state | string | No | Comma-separated: missed, overdue, action_required, needs_invoicing, unscheduled, awaiting_payment. Unscheduled matches draft jobs. |
tag_ids | string | No | Comma-separated tag IDs. Tags must match this resource type. |
Response
200 with { "success": true, "data": [...], "pagination": { "page": 1, "per_page": 25, "total_pages": 1 } }.
See response conventions and errors.
POST /api/v1/jobs
POST /api/v1/jobsRequired scopes: jobs:write.
JSON body
| Field | Type / accepted values | Required | Details |
|---|---|---|---|
name | string or null | No | |
contact_id | integer or null | No | |
property_id | integer or null | No | |
request_id | integer or null | No | |
quote_id | integer or null | No | |
salesperson_id | string or null | No | |
line_items | array of object or object | No | maxItems: 500. |
line_items[]._type | "lineItem" | No | For lineItem rows. Defaults to lineItem when omitted. |
line_items[].name | string | For this variant | For lineItem rows. Item name |
line_items[].description | string or null | No | For lineItem rows. Additional description |
line_items[].quantity | number | No | For lineItem rows. Quantity Default: 1. |
line_items[].unitPrice | number | For this variant | For lineItem rows. Unit price |
line_items[].unitCost | number | No | For lineItem rows. What this costs the business, per unit. Only used when cost tracking is enabled. minimum: 0. |
line_items[].taxable | boolean | No | For lineItem rows. Whether the item is taxable |
line_items[].optional | boolean | No | For lineItem rows. Whether the item is optional |
line_items[]._type | "image" | For this variant | For image rows. |
line_items[].image_path | string | For this variant | For image rows. Storage path returned by /uploads minLength: 1. |
line_items[].image_id | integer or null | No | For image rows. Upload link ID returned by /uploads |
line_items[].description | string or null | No | For image rows. Caption shown beneath the image |
discount | number | No | Default: 0. |
discount_unit | "%", "$" | No | Default: "%". |
deposit | number | No | Default: 0. |
deposit_unit | "%", "$" | No | Default: "%". |
tax_rate | number | No | Default: 0. |
tax_rate_id | integer or null | No | |
total | number | No | Default: 0. |
status | "draft", "scheduled", "in_progress", "completed", "cancelled" | No | Default: "draft". |
type | "one-off", "recurring" | No | Default: "one-off". |
billing_type | "per_visit", "manual" | No | Default: "per_visit". |
invoice_autopay | boolean | No | Default: false. |
invoice_payment_terms | "due_on_receipt", "net_7", "net_14", "net_30", "net_60", "net_90" or null | No | |
waive_credit_card_fee | boolean | No | Skip the workspace's online processing fee for this record. Carries onto invoices created from this job. Default: false. |
expected_end_date | string or null | No | |
expenses | JSON value | No | |
custom_fields | JSON value or null | No |
Unknown body fields are rejected. Conditional requirements are explained above.
Response
201 with { "success": true, "data": ... }. The data contains the result of the operation.
See response conventions and errors.
GET /api/v1/jobs/:id
GET /api/v1/jobs/:idRequired scopes: jobs:read.
Path parameters: id. Use positive integer record IDs from this workspace.
Response
200 with { "success": true, "data": ... }. The data contains the result of the operation.
See response conventions and errors.
PUT /api/v1/jobs/:id
PUT /api/v1/jobs/:idRequired scopes: jobs:write.
Path parameters: id. Use positive integer record IDs from this workspace.
JSON body
| Field | Type / accepted values | Required | Details |
|---|---|---|---|
name | string or null | No | |
contact_id | integer or null | No | |
property_id | integer or null | No | |
request_id | integer or null | No | |
quote_id | integer or null | No | |
salesperson_id | string or null | No | |
line_items | array of object or object | No | maxItems: 500. |
line_items[]._type | "lineItem" | No | For lineItem rows. Defaults to lineItem when omitted. |
line_items[].name | string | For this variant | For lineItem rows. Item name |
line_items[].description | string or null | No | For lineItem rows. Additional description |
line_items[].quantity | number | No | For lineItem rows. Quantity Default: 1. |
line_items[].unitPrice | number | For this variant | For lineItem rows. Unit price |
line_items[].unitCost | number | No | For lineItem rows. What this costs the business, per unit. Only used when cost tracking is enabled. minimum: 0. |
line_items[].taxable | boolean | No | For lineItem rows. Whether the item is taxable |
line_items[].optional | boolean | No | For lineItem rows. Whether the item is optional |
line_items[]._type | "image" | For this variant | For image rows. |
line_items[].image_path | string | For this variant | For image rows. Storage path returned by /uploads minLength: 1. |
line_items[].image_id | integer or null | No | For image rows. Upload link ID returned by /uploads |
line_items[].description | string or null | No | For image rows. Caption shown beneath the image |
discount | number | No | Default: 0. |
discount_unit | "%", "$" | No | Default: "%". |
deposit | number | No | Default: 0. |
deposit_unit | "%", "$" | No | Default: "%". |
tax_rate | number | No | Default: 0. |
tax_rate_id | integer or null | No | |
total | number | No | Default: 0. |
type | "one-off", "recurring" | No | Default: "one-off". |
billing_type | "per_visit", "manual" | No | Default: "per_visit". |
invoice_autopay | boolean | No | Default: false. |
invoice_payment_terms | "due_on_receipt", "net_7", "net_14", "net_30", "net_60", "net_90" or null | No | |
waive_credit_card_fee | boolean | No | Skip the workspace's online processing fee for this record. Carries onto invoices created from this job. Default: false. |
expected_end_date | string or null | No | |
expenses | JSON value | No | |
custom_fields | JSON value or null | No | |
status | "completed", "cancelled", "reopen" | No |
Unknown body fields are rejected. Conditional requirements are explained above.
Response
200 with { "success": true, "data": ... }. The data contains the result of the operation.
See response conventions and errors.
DELETE /api/v1/jobs/:id
DELETE /api/v1/jobs/:idRequired scopes: jobs:write.
Path parameters: id. Use positive integer record IDs from this workspace.
No request body.
Response
200 with { "success": true, "data": ... }. The data contains the result of the operation.
See response conventions and errors.
GET /api/v1/jobs/:id/expenses
GET /api/v1/jobs/:id/expensesRequired scopes: jobs:read.
Path parameters: id. Use positive integer record IDs from this workspace.
Query parameters
| Field | Type / accepted values | Required | Details |
|---|---|---|---|
page | integer | No | Page number, starting at 1. Default: 1. |
per_page | integer | No | Records per page, from 1 to 100. Default: 25. minimum: 1. maximum: 100. |
search | string | No | Search text. maxLength: 100. |
sort_by | string | No | Sort column. See the supported columns below when listed. |
sort_direction | "asc", "desc" | No | Sort direction. Default: "desc". |
Response
200 with { "success": true, "data": [...], "pagination": { "page": 1, "per_page": 25, "total_pages": 1 } }.
See response conventions and errors.
POST /api/v1/jobs/:id/expenses
POST /api/v1/jobs/:id/expensesRequired scopes: jobs:write.
Path parameters: id. Use positive integer record IDs from this workspace.
JSON body
| Field | Type / accepted values | Required | Details |
|---|---|---|---|
name | string | Yes | minLength: 1. |
description | string or null | No | |
date | string or null | No | |
total | number | Yes | |
attachment | string or null | No | |
attachments | array of string | No |
Unknown body fields are rejected. Conditional requirements are explained above.
Response
201 with { "success": true, "data": ... }. The data contains the result of the operation.
See response conventions and errors.
POST /api/v1/jobs/:id/hold
POST /api/v1/jobs/:id/holdRequired scopes: jobs:write, events:write.
Path parameters: id. Use positive integer record IDs from this workspace.
JSON body
| Field | Type / accepted values | Required | Details |
|---|---|---|---|
reason | "customer", "materials", "approval", "weather", "other" | Yes | |
notes | string | No | maxLength: 2000. |
follow_up_on | string | No | Format: date. |
cancel_appointments | boolean | Yes |
Unknown body fields are rejected. Conditional requirements are explained above.
Response
200 with { "success": true, "data": ... }. The data contains the result of the operation.
See response conventions and errors.
POST /api/v1/jobs/:id/resume
POST /api/v1/jobs/:id/resumeRequired scopes: jobs:write, events:write.
Path parameters: id. Use positive integer record IDs from this workspace.
JSON body
| Field | Type / accepted values | Required | Details |
|---|---|---|---|
expected_held_at | string | Yes | Format: date-time. |
resume_on | string | No | Format: date. |
Unknown body fields are rejected. Conditional requirements are explained above.
Response
200 with { "success": true, "data": ... }. The data contains the result of the operation.
See response conventions and errors.
POST /api/v1/jobs/:id/schedule
POST /api/v1/jobs/:id/scheduleRequired scopes: jobs:write, events:write.
Path parameters: id. Use positive integer record IDs from this workspace.
JSON body
| Field | Type / accepted values | Required | Details |
|---|---|---|---|
start_date | string | Yes | First appointment date, YYYY-MM-DD. |
end_date | string or null | No | |
start_time | string or null | No | |
end_time | string or null | No | |
all_day | boolean | No | Default: false. |
multi_day | boolean | No | Default: false. |
is_flexible | boolean | No | Default: false. |
duration_minutes | integer or null | No | |
assignee_ids | array of string | No | Default: []. |
billing_type | "per_visit", "manual" | No | Default: "per_visit". |
payment_terms | "due_on_receipt", "net_7", "net_14", "net_30", "net_60", "net_90" | No | Default: "due_on_receipt". |
repeat_frequency | integer | No | Default: 1. |
repeat_frequency_unit | "daily", "weekly", "monthly", "yearly", "custom", "forever", "occurrences" | No | Default: "weekly". |
repeat_frequency_unit_custom | "daily", "weekly", "monthly", "yearly", "custom", "forever", "occurrences" | No | Default: "weekly". |
repeat_frequency_schedule | object or null | No | |
repeat_frequency_schedule.days | array of integer | When parent supplied | |
repeat_frequency_schedule.type | "weekDays", "monthWeekDays", "monthDays" | When parent supplied | |
repeat_duration | integer or null | No | |
repeat_duration_unit | "daily", "weekly", "monthly", "yearly", "custom", "forever", "occurrences" | No | Default: "forever". |
Unknown body fields are rejected. Conditional requirements are explained above.
Response
201 with { "success": true, "data": ... }. The data contains the result of the operation.
See response conventions and errors.
PUT /api/v1/jobs/:id/status
PUT /api/v1/jobs/:id/statusRequired scopes: jobs:write.
Path parameters: id. Use positive integer record IDs from this workspace.
JSON body
| Field | Type / accepted values | Required | Details |
|---|---|---|---|
status | "completed", "cancelled", "reopen" | Yes |
Unknown body fields are rejected. Conditional requirements are explained above.
Response
200 with { "success": true, "data": ... }. The data contains the result of the operation.
See response conventions and errors.
GET /api/v1/jobs/:id/expenses/:expenseId
GET /api/v1/jobs/:id/expenses/:expenseIdRequired scopes: jobs:read.
Path parameters: id, expenseId. Use positive integer record IDs from this workspace.
Response
200 with { "success": true, "data": ... }. The data contains the result of the operation.
See response conventions and errors.
PUT /api/v1/jobs/:id/expenses/:expenseId
PUT /api/v1/jobs/:id/expenses/:expenseIdRequired scopes: jobs:write.
Path parameters: id, expenseId. Use positive integer record IDs from this workspace.
JSON body
| Field | Type / accepted values | Required | Details |
|---|---|---|---|
name | string | No | minLength: 1. |
description | string or null | No | |
date | string or null | No | |
total | number | No | |
attachment | string or null | No | |
attachments | array of string | No |
Unknown body fields are rejected. Conditional requirements are explained above.
Response
200 with { "success": true, "data": ... }. The data contains the result of the operation.
See response conventions and errors.
DELETE /api/v1/jobs/:id/expenses/:expenseId
DELETE /api/v1/jobs/:id/expenses/:expenseIdRequired scopes: jobs:write.
Path parameters: id, expenseId. Use positive integer record IDs from this workspace.
No request body.
Response
200 with { "success": true, "data": ... }. The data contains the result of the operation.
See response conventions and errors.
POST /api/v1/jobs/:id/schedule/end
POST /api/v1/jobs/:id/schedule/endRequired scopes: jobs:write, events:write.
Path parameters: id. Use positive integer record IDs from this workspace.
JSON body
| Field | Type / accepted values | Required | Details |
|---|---|---|---|
end_date | string | Yes | Last date the schedule should run. Visits after this are removed. Pattern: ^\d{4}-\d{2}-\d{2}$. |
Unknown body fields are rejected. Conditional requirements are explained above.
Response
200 with { "success": true, "data": ... }. The data contains the result of the operation.