Work orders
The shop's jobs on aircraft: one record per job, from the owner's call to the invoice. Owners, admins and technicians only.
19 endpoints · base URL https://api.aerscheduler.com
/work-ordersList work orders
Owners, admins and technicians only (not dispatchers), on any aircraft, because a job carries the owner and money; everybody else gets 403 for any request. Defaults to the open jobs, oldest first (the job board); state=closed lists completed and cancelled jobs newest first.
Query parameters
state | "open" | "closed" | "all" | open (default), closed, or all. |
resourceId | string | Comma-separated aircraft ids: those aircraft's jobs. |
billToOrgUserId | string | Comma-separated member ids: the jobs billed to any of them. |
ownerOrgUserId | string | Comma-separated member ids: jobs on aircraft any of them owns, billed to them or not. |
technicianOrgUserId | string | Comma-separated member ids: jobs with any of them assigned as a technician. |
locationId | string | Comma-separated location ids: jobs on aircraft based at any of them. |
q | string | Matches the job number (1042, WO-1042), the tail number, the person billed, or the request. |
limit | integer | Rows to return, 1,1000. Defaults to 1000; a larger value is clamped rather than rejected. |
offset | integer | Rows to skip, for paging. Defaults to 0. |
sort | string | Field to order by before paging, as a dot path into the row , total, user.firstName. Omit to keep the endpoint's own order. Numbers and ISO timestamps order as numbers and instants, not as text, and empty values always sort last regardless of direction. Ordering happens before the page is cut, so it orders the whole collection rather than the page you are holding. |
order | "asc" | "desc" | asc (default) or desc. Only meaningful with sort. |
Responses
200 | OK | The jobs.→ { data: WorkOrder[] } |
401 | Unauthorized | No token, an expired token, or a malformed one. Sign in again. |
403 | Forbidden | Authenticated, but not allowed to do this. |
429 | Too Many Requests | Rate limited. Retry-After says how long to wait. |
curl https://api.aerscheduler.com/work-orders \
-H "Authorization: Bearer $AERSCHEDULER_KEY"/work-ordersOpen a work order
Owners, admins and technicians only. Opens a job on an aircraft and gives it the organization's next number (from WO-1001). The person billed defaults to the aircraft's primary owner; send billToOrgUserId: null for the organization's own aircraft. Every id in the body must belong to this organization.
Request body
The job.
resourceIdrequired | integer | The aircraft. |
status | "requested" | "scheduled" | "received" | "in_progress" | "waiting_owner" | "waiting_parts" | "ready" | The stage a new job starts in; it starts open (never completed or cancelled). Defaults to requested. An in-the-shop stage stamps receivedAt. |
complaint | string | null | What the owner asked for, in their words. |
receivedAt | string | null | ISO 8601 with an offset, e.g. 2026-10-14T09:30:00Z. Refused in the future, on a job that is requested or scheduled, and as null while the aircraft is at the shop. |
promisedOn | string | null | The day the shop told the owner it would be ready. |
completedAt | string | null | ISO 8601 with an offset. Only a completed job has one, it cannot be cleared while the job is completed, and it is refused in the future or before receivedAt. |
hobbsIn | integer | null | Hobbs at arrival, in TENTHS of an hour (1234.5 hours is 12345). A snapshot: never writes the aircraft's own meter. |
tachIn | integer | null | Tach at arrival, in tenths. |
hobbsOut | integer | null | Hobbs at return to service, in tenths. |
tachOut | integer | null | Tach at return to service, in tenths. |
customerNotes | string | null | Notes the owner may be shown. |
internalNotes | string | null | The shop's own notes. |
billToOrgUserId | integer | null | Who is billed: a member of this organization, usually the aircraft's owner. Null on the organization's own aircraft. Cannot change while an invoice is live. Chosen by an admin; a technician gets the aircraft's owner (403 otherwise). |
reservationId | integer | null | A maintenance booking on the same aircraft that holds the hangar slot. |
technicianOrgUserIds | integer[] | People with the technician role assigned. Replaces the whole list; somebody already on the job who lost the role may stay. |
Responses
201 | Created | The new job.→ { data: WorkOrder } |
400 | Bad Request | A field failed validation, or an id is not in this organization. |
401 | Unauthorized | No token, an expired token, or a malformed one. Sign in again. |
403 | Forbidden | Authenticated, but not allowed to do this. |
429 | Too Many Requests | Rate limited. Retry-After says how long to wait. |
curl -X POST https://api.aerscheduler.com/work-orders \
-H "Authorization: Bearer $AERSCHEDULER_KEY" \
-H "Content-Type: application/json" \
-d '{"resourceId":1}'/work-orders/{id}Get a work order
Owners, admins and technicians only. 404 for an id that is not one of this organization's jobs.
Path parameters
idrequired | integer | The work order id. |
Responses
200 | OK | The job.→ { data: WorkOrder } |
401 | Unauthorized | No token, an expired token, or a malformed one. Sign in again. |
403 | Forbidden | Authenticated, but not allowed to do this. |
404 | Not Found | Not one of this organization's jobs. |
429 | Too Many Requests | Rate limited. Retry-After says how long to wait. |
curl https://api.aerscheduler.com/work-orders/:id \
-H "Authorization: Bearer $AERSCHEDULER_KEY"/work-orders/{id}Change a work order
Owners, admins and technicians only. Only the fields sent move. Everything a person changes is recorded in the audit trail, with the job's number, aircraft and the person billed.
Path parameters
idrequired | integer | The work order id. |
Request body
Any subset.
status | "requested" | "scheduled" | "received" | "in_progress" | "waiting_owner" | "waiting_parts" | "ready" | "completed" | "cancelled" | The stage the shop sets. Moving to an in-the-shop stage stamps receivedAt, and moving back to requested or scheduled clears it; completed stamps completedAt; reopening clears it. A job with a live invoice cannot be cancelled. |
complaint | string | null | What the owner asked for, in their words. |
receivedAt | string | null | ISO 8601 with an offset, e.g. 2026-10-14T09:30:00Z. Refused in the future, on a job that is requested or scheduled, and as null while the aircraft is at the shop. |
promisedOn | string | null | The day the shop told the owner it would be ready. |
completedAt | string | null | ISO 8601 with an offset. Only a completed job has one, it cannot be cleared while the job is completed, and it is refused in the future or before receivedAt. |
hobbsIn | integer | null | Hobbs at arrival, in TENTHS of an hour (1234.5 hours is 12345). A snapshot: never writes the aircraft's own meter. |
tachIn | integer | null | Tach at arrival, in tenths. |
hobbsOut | integer | null | Hobbs at return to service, in tenths. |
tachOut | integer | null | Tach at return to service, in tenths. |
customerNotes | string | null | Notes the owner may be shown. |
internalNotes | string | null | The shop's own notes. |
billToOrgUserId | integer | null | Who is billed: a member of this organization, usually the aircraft's owner. Null on the organization's own aircraft. Cannot change while an invoice is live. Chosen by an admin; a technician gets the aircraft's owner (403 otherwise). |
reservationId | integer | null | A maintenance booking on the same aircraft that holds the hangar slot. |
technicianOrgUserIds | integer[] | People with the technician role assigned. Replaces the whole list; somebody already on the job who lost the role may stay. |
Responses
200 | OK | The job as it now stands.→ { data: WorkOrder } |
400 | Bad Request | A field failed validation, an id is not in this organization, or the change is refused while an invoice is live. |
401 | Unauthorized | No token, an expired token, or a malformed one. Sign in again. |
403 | Forbidden | Authenticated, but not allowed to do this. |
404 | Not Found | Not one of this organization's jobs. |
429 | Too Many Requests | Rate limited. Retry-After says how long to wait. |
curl -X PATCH https://api.aerscheduler.com/work-orders/:id \
-H "Authorization: Bearer $AERSCHEDULER_KEY" \
-H "Content-Type: application/json" \
-d '{"status":"requested","complaint":"…"}'/work-orders/{id}Delete a work order
Admins only. For a job opened by mistake. A job that has ever had an invoice, voided or not, stays on the record: cancel it instead (status: cancelled). The audit trail keeps a line saying it existed.
Path parameters
idrequired | integer | The work order id. |
Responses
204 | No Content | Success. No body. |
400 | Bad Request | The job has been invoiced. |
401 | Unauthorized | No token, an expired token, or a malformed one. Sign in again. |
403 | Forbidden | Not an admin. |
404 | Not Found | Not one of this organization's jobs. |
429 | Too Many Requests | Rate limited. Retry-After says how long to wait. |
curl -X DELETE https://api.aerscheduler.com/work-orders/:id \
-H "Authorization: Bearer $AERSCHEDULER_KEY"/work-orders/settingsGet the shop's rates
Owners, admins and technicians only. The labor rate and default markups that price new lines.
Responses
200 | OK | The rates.→ { data: WorkOrderSettings } |
401 | Unauthorized | No token, an expired token, or a malformed one. Sign in again. |
403 | Forbidden | Authenticated, but not allowed to do this. |
429 | Too Many Requests | Rate limited. Retry-After says how long to wait. |
curl https://api.aerscheduler.com/work-orders/settings \
-H "Authorization: Bearer $AERSCHEDULER_KEY"/work-orders/settingsChange the shop's rates
Admins only. Any subset. Lines already on jobs keep their price.
Request body
Any subset.
laborRateCents | integer | null | |
partsMarkupBps | integer | null | |
outsideWorkMarkupBps | integer | null |
Responses
200 | OK | The rates.→ { data: WorkOrderSettings } |
400 | Bad Request | Failed validation. |
401 | Unauthorized | No token, an expired token, or a malformed one. Sign in again. |
403 | Forbidden | Not an admin. |
429 | Too Many Requests | Rate limited. Retry-After says how long to wait. |
curl -X PUT https://api.aerscheduler.com/work-orders/settings \
-H "Authorization: Bearer $AERSCHEDULER_KEY" \
-H "Content-Type: application/json" \
-d '{"laborRateCents":1,"partsMarkupBps":1}'/work-orders/{id}/approvalsList the owner's answers
Owners, admins and technicians only. The phone calls recorded on a job, newest first.
Path parameters
idrequired | integer | The work order id. |
Query parameters
limit | integer | Rows to return, 1,1000. Defaults to 1000; a larger value is clamped rather than rejected. |
offset | integer | Rows to skip, for paging. Defaults to 0. |
sort | string | Field to order by before paging, as a dot path into the row , total, user.firstName. Omit to keep the endpoint's own order. Numbers and ISO timestamps order as numbers and instants, not as text, and empty values always sort last regardless of direction. Ordering happens before the page is cut, so it orders the whole collection rather than the page you are holding. |
order | "asc" | "desc" | asc (default) or desc. Only meaningful with sort. |
Responses
200 | OK | The calls.→ { data: WorkOrderApproval[] } |
401 | Unauthorized | No token, an expired token, or a malformed one. Sign in again. |
403 | Forbidden | Authenticated, but not allowed to do this. |
404 | Not Found | Not one of this organization's jobs. |
429 | Too Many Requests | Rate limited. Retry-After says how long to wait. |
curl https://api.aerscheduler.com/work-orders/:id/approvals \
-H "Authorization: Bearer $AERSCHEDULER_KEY"/work-orders/{id}/approvalsRecord the owner's answer
Owners, admins and technicians only. A record of a phone call, not an owner approval flow: who was reached, when (defaults to now), the spend limit, notes, and approved, declined or deferred for each item discussed. Every item must be on this job.
Path parameters
idrequired | integer | The work order id. |
Request body
The call.
contactNamerequired | string | |
contactedAt | string | ISO 8601 with an offset. Refused in the future. |
spendLimitCents | integer | null | |
notes | string | null | |
decisionsrequired | object[] |
Responses
201 | Created | The call's id.→ { data: object } |
400 | Bad Request | Failed validation, or an item is not on this job. |
401 | Unauthorized | No token, an expired token, or a malformed one. Sign in again. |
403 | Forbidden | Authenticated, but not allowed to do this. |
404 | Not Found | Not one of this organization's jobs. |
429 | Too Many Requests | Rate limited. Retry-After says how long to wait. |
curl -X POST https://api.aerscheduler.com/work-orders/:id/approvals \
-H "Authorization: Bearer $AERSCHEDULER_KEY" \
-H "Content-Type: application/json" \
-d '{"contactName":"…","decisions":[]}'/work-orders/{id}/invoiceRaise a job's invoice
Admins only. Raises the job's invoice to the person billed, as a work order bill linked to the job, through Stripe in both billing modes (never onto an account balance), with sales tax by the organization's rules. One live invoice per job: void it to raise another. Send expectedTotal and expectedDetails from the preview; a 409 carries the current total if either moved. Refused in the public demo.
Path parameters
idrequired | integer | The work order id. |
Request body
Optional.
expectedTotal | integer | The total the preview showed, in cents. Anything but a whole number is refused. |
expectedDetails | string | The preview's details.hash: refused with a 409 (data.details true) if the notes for the owner or the finished work changed since. |
dueIn | integer | Days until due. |
Responses
201 | Created | The invoice.→ { data: Invoice } |
400 | Bad Request | Nobody billed, nothing billable, an owner with no address, or the job already has an invoice. |
401 | Unauthorized | No token, an expired token, or a malformed one. Sign in again. |
403 | Forbidden | Not an admin, or the demo. |
404 | Not Found | Not one of this organization's jobs. |
409 | With data.total: the total (or, with data.details, the invoice's text) moved since it was priced, and that is the current total. Without it: the job changed while the invoice was being made, and that invoice was voided (or Stripe would not void it: void it in Billing). | |
429 | Too Many Requests | Rate limited. Retry-After says how long to wait. |
curl -X POST https://api.aerscheduler.com/work-orders/:id/invoice \
-H "Authorization: Bearer $AERSCHEDULER_KEY" \
-H "Content-Type: application/json" \
-d '{"expectedTotal":1,"expectedDetails":"…"}'/work-orders/{id}/itemsList a job's items
Owners, admins and technicians only. In order. 404 for a job that is not this organization's.
Path parameters
idrequired | integer | The work order id. |
Query parameters
limit | integer | Rows to return, 1,1000. Defaults to 1000; a larger value is clamped rather than rejected. |
offset | integer | Rows to skip, for paging. Defaults to 0. |
sort | string | Field to order by before paging, as a dot path into the row , total, user.firstName. Omit to keep the endpoint's own order. Numbers and ISO timestamps order as numbers and instants, not as text, and empty values always sort last regardless of direction. Ordering happens before the page is cut, so it orders the whole collection rather than the page you are holding. |
order | "asc" | "desc" | asc (default) or desc. Only meaningful with sort. |
Responses
200 | OK | The items.→ { data: WorkOrderItem[] } |
401 | Unauthorized | No token, an expired token, or a malformed one. Sign in again. |
403 | Forbidden | Authenticated, but not allowed to do this. |
404 | Not Found | Not one of this organization's jobs. |
429 | Too Many Requests | Rate limited. Retry-After says how long to wait. |
curl https://api.aerscheduler.com/work-orders/:id/items \
-H "Authorization: Bearer $AERSCHEDULER_KEY"/work-orders/{id}/itemsAdd an item to a job
Owners, admins and technicians only. Something the owner asked for (requested) or the shop found (found). Attach an open inspection or squawk on the same aircraft and the item is done when that is signed off or resolved.
Path parameters
idrequired | integer | The work order id. |
Request body
The item.
sourcerequired | "requested" | "found" | |
descriptionrequired | string | |
maintenanceReminderId | integer | An open inspection on the job's aircraft. |
squawkId | integer | An open squawk on the job's aircraft. |
Responses
201 | Created | The item.→ { data: WorkOrderItem } |
400 | Bad Request | Failed validation. |
401 | Unauthorized | No token, an expired token, or a malformed one. Sign in again. |
403 | Forbidden | Authenticated, but not allowed to do this. |
404 | Not Found | Not one of this organization's jobs. |
429 | Too Many Requests | Rate limited. Retry-After says how long to wait. |
curl -X POST https://api.aerscheduler.com/work-orders/:id/items \
-H "Authorization: Bearer $AERSCHEDULER_KEY" \
-H "Content-Type: application/json" \
-d '{"source":"requested","description":"…"}'/work-orders/{id}/linesList a job's lines
Owners, admins and technicians only. In order.
Path parameters
idrequired | integer | The work order id. |
Query parameters
limit | integer | Rows to return, 1,1000. Defaults to 1000; a larger value is clamped rather than rejected. |
offset | integer | Rows to skip, for paging. Defaults to 0. |
sort | string | Field to order by before paging, as a dot path into the row , total, user.firstName. Omit to keep the endpoint's own order. Numbers and ISO timestamps order as numbers and instants, not as text, and empty values always sort last regardless of direction. Ordering happens before the page is cut, so it orders the whole collection rather than the page you are holding. |
order | "asc" | "desc" | asc (default) or desc. Only meaningful with sort. |
Responses
200 | OK | The lines.→ { data: WorkOrderLine[] } |
401 | Unauthorized | No token, an expired token, or a malformed one. Sign in again. |
403 | Forbidden | Authenticated, but not allowed to do this. |
404 | Not Found | Not one of this organization's jobs. |
429 | Too Many Requests | Rate limited. Retry-After says how long to wait. |
curl https://api.aerscheduler.com/work-orders/:id/lines \
-H "Authorization: Bearer $AERSCHEDULER_KEY"/work-orders/{id}/linesAdd a line
Owners, admins and technicians only. Labor is priced from minutes at rateCents (the shop rate by default); a part or outside work from costCents plus the markup (the organization default by default) unless unitPriceCents is sent. A line can be at most $999,999.99. Refused once the job has a live invoice.
Only an admin prices. Anyone else in the shop sends rateCents, markupBps, billable and taxable only with the shop's defaults, and unitPriceCents only for supplies, freight, fees and other (403 otherwise).
Path parameters
idrequired | integer | The work order id. |
Request body
The line.
categoryrequired | "labor" | "part" | "supply" | "outside_service" | "freight" | "fee" | "other" | |
descriptionrequired | string | |
qty | integer | Not for labor (always 1). |
costCents | integer | null | Parts and outside work: what the shop paid per unit. Priced with the markup when no price is sent. |
markupBps | integer | null | Overrides the organization's default markup (parts and outside work). Admin only. |
unitPriceCents | integer | The customer's price per unit. For labor, a flat charge instead of hours at the rate. |
discountBps | integer | null | A discount on the line in basis points (1000 is 10% off), applied per unit before tax and said on the invoice line. Admin only. |
taxable | boolean | null | Decide the tax on this line; null follows the organization's rule. |
billable | boolean | |
minutes | integer | Labor: clock minutes. |
rateCents | integer | null | Labor: overrides the shop rate. Admin only. |
workedOn | string | null | |
technicianOrgUserId | integer | null | Labor: who did the work, a person with the technician role. |
partNumber | string | null | |
serialNumber | string | null | |
vendor | string | null | |
partStatus | string | null | |
expectedOn | string | null | |
itemId | integer | null | An item on the same job. |
Responses
201 | Created | The line.→ { data: WorkOrderLine } |
400 | Bad Request | Failed validation, or the job is invoiced. |
401 | Unauthorized | No token, an expired token, or a malformed one. Sign in again. |
403 | Forbidden | Authenticated, but not allowed to do this. |
404 | Not Found | Not one of this organization's jobs. |
429 | Too Many Requests | Rate limited. Retry-After says how long to wait. |
curl -X POST https://api.aerscheduler.com/work-orders/:id/lines \
-H "Authorization: Bearer $AERSCHEDULER_KEY" \
-H "Content-Type: application/json" \
-d '{"category":"labor","description":"…"}'/work-orders/{id}/invoice/previewPrice a job's invoice
Admins only. The bill as it would be raised now: the job's billable lines, each with its sales tax, the organization's service fee, and the total. Priced by the same code as the invoice.
Path parameters
idrequired | integer | The work order id. |
Responses
200 | OK | The priced bill.→ { data: object } |
400 | Bad Request | Nobody billed, nothing billable, or the job was cancelled. |
401 | Unauthorized | No token, an expired token, or a malformed one. Sign in again. |
403 | Forbidden | Not an admin. |
404 | Not Found | Not one of this organization's jobs. |
429 | Too Many Requests | Rate limited. Retry-After says how long to wait. |
curl -X POST https://api.aerscheduler.com/work-orders/:id/invoice/preview \
-H "Authorization: Bearer $AERSCHEDULER_KEY"/work-orders/{id}/items/{itemId}Change an item
Owners, admins and technicians only. Reword it, or mark a plain item done (done: true) or not. An item tied to an inspection or squawk is refused: sign off the inspection or resolve the squawk.
Path parameters
idrequired | integer | The work order id. |
itemIdrequired | integer | The item id. |
Request body
Any subset.
description | string | |
done | boolean |
Responses
200 | OK | The item.→ { data: WorkOrderItem } |
400 | Bad Request | Failed validation. |
401 | Unauthorized | No token, an expired token, or a malformed one. Sign in again. |
403 | Forbidden | Authenticated, but not allowed to do this. |
404 | Not Found | Not this job's item. |
429 | Too Many Requests | Rate limited. Retry-After says how long to wait. |
curl -X PATCH https://api.aerscheduler.com/work-orders/:id/items/:itemId \
-H "Authorization: Bearer $AERSCHEDULER_KEY" \
-H "Content-Type: application/json" \
-d '{"description":"…","done":true}'/work-orders/{id}/items/{itemId}Remove an item
Owners, admins and technicians only. Lines charged against it stay on the job.
Path parameters
idrequired | integer | The work order id. |
itemIdrequired | integer | The item id. |
Responses
204 | No Content | Success. No body. |
401 | Unauthorized | No token, an expired token, or a malformed one. Sign in again. |
403 | Forbidden | Authenticated, but not allowed to do this. |
404 | Not Found | Not this job's item. |
429 | Too Many Requests | Rate limited. Retry-After says how long to wait. |
curl -X DELETE https://api.aerscheduler.com/work-orders/:id/items/:itemId \
-H "Authorization: Bearer $AERSCHEDULER_KEY"/work-orders/{id}/lines/{lineId}Change a line
Owners, admins and technicians only. Any subset; the line is re-priced only when its hours, rate, cost or markup CHANGE and no price is sent, so resending them keeps a price set by hand. Refused once the job has a live invoice. Somebody other than an admin changes only the lines they entered, and only as on create.
Path parameters
idrequired | integer | The work order id. |
lineIdrequired | integer | The line id. |
Request body
Any subset.
category | "labor" | "part" | "supply" | "outside_service" | "freight" | "fee" | "other" | |
description | string | |
qty | integer | Not for labor (always 1). |
costCents | integer | null | Parts and outside work: what the shop paid per unit. Priced with the markup when no price is sent. |
markupBps | integer | null | Overrides the organization's default markup (parts and outside work). Admin only. |
unitPriceCents | integer | The customer's price per unit. For labor, a flat charge instead of hours at the rate. |
discountBps | integer | null | A discount on the line in basis points (1000 is 10% off), applied per unit before tax and said on the invoice line. Admin only. |
taxable | boolean | null | Decide the tax on this line; null follows the organization's rule. |
billable | boolean | |
minutes | integer | Labor: clock minutes. |
rateCents | integer | null | Labor: overrides the shop rate. Admin only. |
workedOn | string | null | |
technicianOrgUserId | integer | null | Labor: who did the work, a person with the technician role. |
partNumber | string | null | |
serialNumber | string | null | |
vendor | string | null | |
partStatus | string | null | |
expectedOn | string | null | |
itemId | integer | null | An item on the same job. |
Responses
200 | OK | The line.→ { data: WorkOrderLine } |
400 | Bad Request | Failed validation, or the job is invoiced. |
401 | Unauthorized | No token, an expired token, or a malformed one. Sign in again. |
403 | Forbidden | Authenticated, but not allowed to do this. |
404 | Not Found | Not this job's line. |
429 | Too Many Requests | Rate limited. Retry-After says how long to wait. |
curl -X PATCH https://api.aerscheduler.com/work-orders/:id/lines/:lineId \
-H "Authorization: Bearer $AERSCHEDULER_KEY" \
-H "Content-Type: application/json" \
-d '{"category":"labor","description":"…"}'/work-orders/{id}/lines/{lineId}Remove a line
Owners, admins and technicians only. Refused once the job has a live invoice. Somebody other than an admin removes only the lines they entered.
Path parameters
idrequired | integer | The work order id. |
lineIdrequired | integer | The line id. |
Responses
204 | No Content | Success. No body. |
400 | Bad Request | The job is invoiced. |
401 | Unauthorized | No token, an expired token, or a malformed one. Sign in again. |
403 | Forbidden | Authenticated, but not allowed to do this. |
404 | Not Found | Not this job's line. |
429 | Too Many Requests | Rate limited. Retry-After says how long to wait. |
curl -X DELETE https://api.aerscheduler.com/work-orders/:id/lines/:lineId \
-H "Authorization: Bearer $AERSCHEDULER_KEY"