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

get/work-orders

List 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.
resourceIdstringComma-separated aircraft ids: those aircraft's jobs.
billToOrgUserIdstringComma-separated member ids: the jobs billed to any of them.
ownerOrgUserIdstringComma-separated member ids: jobs on aircraft any of them owns, billed to them or not.
technicianOrgUserIdstringComma-separated member ids: jobs with any of them assigned as a technician.
locationIdstringComma-separated location ids: jobs on aircraft based at any of them.
qstringMatches the job number (1042, WO-1042), the tail number, the person billed, or the request.
limitintegerRows to return, 1,1000. Defaults to 1000; a larger value is clamped rather than rejected.
offsetintegerRows to skip, for paging. Defaults to 0.
sortstringField 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

200OKThe jobs.→ { data: WorkOrder[] }
401UnauthorizedNo token, an expired token, or a malformed one. Sign in again.
403ForbiddenAuthenticated, but not allowed to do this.
429Too Many RequestsRate limited. Retry-After says how long to wait.
Example request
curl https://api.aerscheduler.com/work-orders \
  -H "Authorization: Bearer $AERSCHEDULER_KEY"
post/work-orders

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

resourceIdrequiredintegerThe 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.
complaintstring | nullWhat the owner asked for, in their words.
receivedAtstring | nullISO 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.
promisedOnstring | nullThe day the shop told the owner it would be ready.
completedAtstring | nullISO 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.
hobbsIninteger | nullHobbs at arrival, in TENTHS of an hour (1234.5 hours is 12345). A snapshot: never writes the aircraft's own meter.
tachIninteger | nullTach at arrival, in tenths.
hobbsOutinteger | nullHobbs at return to service, in tenths.
tachOutinteger | nullTach at return to service, in tenths.
customerNotesstring | nullNotes the owner may be shown.
internalNotesstring | nullThe shop's own notes.
billToOrgUserIdinteger | nullWho 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).
reservationIdinteger | nullA maintenance booking on the same aircraft that holds the hangar slot.
technicianOrgUserIdsinteger[]People with the technician role assigned. Replaces the whole list; somebody already on the job who lost the role may stay.

Responses

201CreatedThe new job.→ { data: WorkOrder }
400Bad RequestA field failed validation, or an id is not in this organization.
401UnauthorizedNo token, an expired token, or a malformed one. Sign in again.
403ForbiddenAuthenticated, but not allowed to do this.
429Too Many RequestsRate limited. Retry-After says how long to wait.
Example request
curl -X POST https://api.aerscheduler.com/work-orders \
  -H "Authorization: Bearer $AERSCHEDULER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"resourceId":1}'
get/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

idrequiredintegerThe work order id.

Responses

200OKThe job.→ { data: WorkOrder }
401UnauthorizedNo token, an expired token, or a malformed one. Sign in again.
403ForbiddenAuthenticated, but not allowed to do this.
404Not FoundNot one of this organization's jobs.
429Too Many RequestsRate limited. Retry-After says how long to wait.
Example request
curl https://api.aerscheduler.com/work-orders/:id \
  -H "Authorization: Bearer $AERSCHEDULER_KEY"
patch/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

idrequiredintegerThe 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.
complaintstring | nullWhat the owner asked for, in their words.
receivedAtstring | nullISO 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.
promisedOnstring | nullThe day the shop told the owner it would be ready.
completedAtstring | nullISO 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.
hobbsIninteger | nullHobbs at arrival, in TENTHS of an hour (1234.5 hours is 12345). A snapshot: never writes the aircraft's own meter.
tachIninteger | nullTach at arrival, in tenths.
hobbsOutinteger | nullHobbs at return to service, in tenths.
tachOutinteger | nullTach at return to service, in tenths.
customerNotesstring | nullNotes the owner may be shown.
internalNotesstring | nullThe shop's own notes.
billToOrgUserIdinteger | nullWho 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).
reservationIdinteger | nullA maintenance booking on the same aircraft that holds the hangar slot.
technicianOrgUserIdsinteger[]People with the technician role assigned. Replaces the whole list; somebody already on the job who lost the role may stay.

Responses

200OKThe job as it now stands.→ { data: WorkOrder }
400Bad RequestA field failed validation, an id is not in this organization, or the change is refused while an invoice is live.
401UnauthorizedNo token, an expired token, or a malformed one. Sign in again.
403ForbiddenAuthenticated, but not allowed to do this.
404Not FoundNot one of this organization's jobs.
429Too Many RequestsRate limited. Retry-After says how long to wait.
Example request
curl -X PATCH https://api.aerscheduler.com/work-orders/:id \
  -H "Authorization: Bearer $AERSCHEDULER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status":"requested","complaint":"…"}'
delete/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

idrequiredintegerThe work order id.

Responses

204No ContentSuccess. No body.
400Bad RequestThe job has been invoiced.
401UnauthorizedNo token, an expired token, or a malformed one. Sign in again.
403ForbiddenNot an admin.
404Not FoundNot one of this organization's jobs.
429Too Many RequestsRate limited. Retry-After says how long to wait.
Example request
curl -X DELETE https://api.aerscheduler.com/work-orders/:id \
  -H "Authorization: Bearer $AERSCHEDULER_KEY"
get/work-orders/settings

Get the shop's rates

Owners, admins and technicians only. The labor rate and default markups that price new lines.

Responses

200OKThe rates.→ { data: WorkOrderSettings }
401UnauthorizedNo token, an expired token, or a malformed one. Sign in again.
403ForbiddenAuthenticated, but not allowed to do this.
429Too Many RequestsRate limited. Retry-After says how long to wait.
Example request
curl https://api.aerscheduler.com/work-orders/settings \
  -H "Authorization: Bearer $AERSCHEDULER_KEY"
put/work-orders/settings

Change the shop's rates

Admins only. Any subset. Lines already on jobs keep their price.

Request body

Any subset.

laborRateCentsinteger | null
partsMarkupBpsinteger | null
outsideWorkMarkupBpsinteger | null

Responses

200OKThe rates.→ { data: WorkOrderSettings }
400Bad RequestFailed validation.
401UnauthorizedNo token, an expired token, or a malformed one. Sign in again.
403ForbiddenNot an admin.
429Too Many RequestsRate limited. Retry-After says how long to wait.
Example request
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}'
get/work-orders/{id}/approvals

List the owner's answers

Owners, admins and technicians only. The phone calls recorded on a job, newest first.

Path parameters

idrequiredintegerThe work order id.

Query parameters

limitintegerRows to return, 1,1000. Defaults to 1000; a larger value is clamped rather than rejected.
offsetintegerRows to skip, for paging. Defaults to 0.
sortstringField 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

200OKThe calls.→ { data: WorkOrderApproval[] }
401UnauthorizedNo token, an expired token, or a malformed one. Sign in again.
403ForbiddenAuthenticated, but not allowed to do this.
404Not FoundNot one of this organization's jobs.
429Too Many RequestsRate limited. Retry-After says how long to wait.
Example request
curl https://api.aerscheduler.com/work-orders/:id/approvals \
  -H "Authorization: Bearer $AERSCHEDULER_KEY"
post/work-orders/{id}/approvals

Record 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

idrequiredintegerThe work order id.

Request body

The call.

contactNamerequiredstring
contactedAtstringISO 8601 with an offset. Refused in the future.
spendLimitCentsinteger | null
notesstring | null
decisionsrequiredobject[]

Responses

201CreatedThe call's id.→ { data: object }
400Bad RequestFailed validation, or an item is not on this job.
401UnauthorizedNo token, an expired token, or a malformed one. Sign in again.
403ForbiddenAuthenticated, but not allowed to do this.
404Not FoundNot one of this organization's jobs.
429Too Many RequestsRate limited. Retry-After says how long to wait.
Example request
curl -X POST https://api.aerscheduler.com/work-orders/:id/approvals \
  -H "Authorization: Bearer $AERSCHEDULER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"contactName":"…","decisions":[]}'
post/work-orders/{id}/invoice

Raise 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

idrequiredintegerThe work order id.

Request body

Optional.

expectedTotalintegerThe total the preview showed, in cents. Anything but a whole number is refused.
expectedDetailsstringThe preview's details.hash: refused with a 409 (data.details true) if the notes for the owner or the finished work changed since.
dueInintegerDays until due.

Responses

201CreatedThe invoice.→ { data: Invoice }
400Bad RequestNobody billed, nothing billable, an owner with no address, or the job already has an invoice.
401UnauthorizedNo token, an expired token, or a malformed one. Sign in again.
403ForbiddenNot an admin, or the demo.
404Not FoundNot one of this organization's jobs.
409With 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).
429Too Many RequestsRate limited. Retry-After says how long to wait.
Example request
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":"…"}'
get/work-orders/{id}/items

List a job's items

Owners, admins and technicians only. In order. 404 for a job that is not this organization's.

Path parameters

idrequiredintegerThe work order id.

Query parameters

limitintegerRows to return, 1,1000. Defaults to 1000; a larger value is clamped rather than rejected.
offsetintegerRows to skip, for paging. Defaults to 0.
sortstringField 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

200OKThe items.→ { data: WorkOrderItem[] }
401UnauthorizedNo token, an expired token, or a malformed one. Sign in again.
403ForbiddenAuthenticated, but not allowed to do this.
404Not FoundNot one of this organization's jobs.
429Too Many RequestsRate limited. Retry-After says how long to wait.
Example request
curl https://api.aerscheduler.com/work-orders/:id/items \
  -H "Authorization: Bearer $AERSCHEDULER_KEY"
post/work-orders/{id}/items

Add 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

idrequiredintegerThe work order id.

Request body

The item.

sourcerequired"requested" | "found"
descriptionrequiredstring
maintenanceReminderIdintegerAn open inspection on the job's aircraft.
squawkIdintegerAn open squawk on the job's aircraft.

Responses

201CreatedThe item.→ { data: WorkOrderItem }
400Bad RequestFailed validation.
401UnauthorizedNo token, an expired token, or a malformed one. Sign in again.
403ForbiddenAuthenticated, but not allowed to do this.
404Not FoundNot one of this organization's jobs.
429Too Many RequestsRate limited. Retry-After says how long to wait.
Example request
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":"…"}'
get/work-orders/{id}/lines

List a job's lines

Owners, admins and technicians only. In order.

Path parameters

idrequiredintegerThe work order id.

Query parameters

limitintegerRows to return, 1,1000. Defaults to 1000; a larger value is clamped rather than rejected.
offsetintegerRows to skip, for paging. Defaults to 0.
sortstringField 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

200OKThe lines.→ { data: WorkOrderLine[] }
401UnauthorizedNo token, an expired token, or a malformed one. Sign in again.
403ForbiddenAuthenticated, but not allowed to do this.
404Not FoundNot one of this organization's jobs.
429Too Many RequestsRate limited. Retry-After says how long to wait.
Example request
curl https://api.aerscheduler.com/work-orders/:id/lines \
  -H "Authorization: Bearer $AERSCHEDULER_KEY"
post/work-orders/{id}/lines

Add 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

idrequiredintegerThe work order id.

Request body

The line.

categoryrequired"labor" | "part" | "supply" | "outside_service" | "freight" | "fee" | "other"
descriptionrequiredstring
qtyintegerNot for labor (always 1).
costCentsinteger | nullParts and outside work: what the shop paid per unit. Priced with the markup when no price is sent.
markupBpsinteger | nullOverrides the organization's default markup (parts and outside work). Admin only.
unitPriceCentsintegerThe customer's price per unit. For labor, a flat charge instead of hours at the rate.
discountBpsinteger | nullA discount on the line in basis points (1000 is 10% off), applied per unit before tax and said on the invoice line. Admin only.
taxableboolean | nullDecide the tax on this line; null follows the organization's rule.
billableboolean
minutesintegerLabor: clock minutes.
rateCentsinteger | nullLabor: overrides the shop rate. Admin only.
workedOnstring | null
technicianOrgUserIdinteger | nullLabor: who did the work, a person with the technician role.
partNumberstring | null
serialNumberstring | null
vendorstring | null
partStatusstring | null
expectedOnstring | null
itemIdinteger | nullAn item on the same job.

Responses

201CreatedThe line.→ { data: WorkOrderLine }
400Bad RequestFailed validation, or the job is invoiced.
401UnauthorizedNo token, an expired token, or a malformed one. Sign in again.
403ForbiddenAuthenticated, but not allowed to do this.
404Not FoundNot one of this organization's jobs.
429Too Many RequestsRate limited. Retry-After says how long to wait.
Example request
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":"…"}'
post/work-orders/{id}/invoice/preview

Price 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

idrequiredintegerThe work order id.

Responses

200OKThe priced bill.→ { data: object }
400Bad RequestNobody billed, nothing billable, or the job was cancelled.
401UnauthorizedNo token, an expired token, or a malformed one. Sign in again.
403ForbiddenNot an admin.
404Not FoundNot one of this organization's jobs.
429Too Many RequestsRate limited. Retry-After says how long to wait.
Example request
curl -X POST https://api.aerscheduler.com/work-orders/:id/invoice/preview \
  -H "Authorization: Bearer $AERSCHEDULER_KEY"
patch/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

idrequiredintegerThe work order id.
itemIdrequiredintegerThe item id.

Request body

Any subset.

descriptionstring
doneboolean

Responses

200OKThe item.→ { data: WorkOrderItem }
400Bad RequestFailed validation.
401UnauthorizedNo token, an expired token, or a malformed one. Sign in again.
403ForbiddenAuthenticated, but not allowed to do this.
404Not FoundNot this job's item.
429Too Many RequestsRate limited. Retry-After says how long to wait.
Example request
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}'
delete/work-orders/{id}/items/{itemId}

Remove an item

Owners, admins and technicians only. Lines charged against it stay on the job.

Path parameters

idrequiredintegerThe work order id.
itemIdrequiredintegerThe item id.

Responses

204No ContentSuccess. No body.
401UnauthorizedNo token, an expired token, or a malformed one. Sign in again.
403ForbiddenAuthenticated, but not allowed to do this.
404Not FoundNot this job's item.
429Too Many RequestsRate limited. Retry-After says how long to wait.
Example request
curl -X DELETE https://api.aerscheduler.com/work-orders/:id/items/:itemId \
  -H "Authorization: Bearer $AERSCHEDULER_KEY"
patch/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

idrequiredintegerThe work order id.
lineIdrequiredintegerThe line id.

Request body

Any subset.

category"labor" | "part" | "supply" | "outside_service" | "freight" | "fee" | "other"
descriptionstring
qtyintegerNot for labor (always 1).
costCentsinteger | nullParts and outside work: what the shop paid per unit. Priced with the markup when no price is sent.
markupBpsinteger | nullOverrides the organization's default markup (parts and outside work). Admin only.
unitPriceCentsintegerThe customer's price per unit. For labor, a flat charge instead of hours at the rate.
discountBpsinteger | nullA discount on the line in basis points (1000 is 10% off), applied per unit before tax and said on the invoice line. Admin only.
taxableboolean | nullDecide the tax on this line; null follows the organization's rule.
billableboolean
minutesintegerLabor: clock minutes.
rateCentsinteger | nullLabor: overrides the shop rate. Admin only.
workedOnstring | null
technicianOrgUserIdinteger | nullLabor: who did the work, a person with the technician role.
partNumberstring | null
serialNumberstring | null
vendorstring | null
partStatusstring | null
expectedOnstring | null
itemIdinteger | nullAn item on the same job.

Responses

200OKThe line.→ { data: WorkOrderLine }
400Bad RequestFailed validation, or the job is invoiced.
401UnauthorizedNo token, an expired token, or a malformed one. Sign in again.
403ForbiddenAuthenticated, but not allowed to do this.
404Not FoundNot this job's line.
429Too Many RequestsRate limited. Retry-After says how long to wait.
Example request
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":"…"}'
delete/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

idrequiredintegerThe work order id.
lineIdrequiredintegerThe line id.

Responses

204No ContentSuccess. No body.
400Bad RequestThe job is invoiced.
401UnauthorizedNo token, an expired token, or a malformed one. Sign in again.
403ForbiddenAuthenticated, but not allowed to do this.
404Not FoundNot this job's line.
429Too Many RequestsRate limited. Retry-After says how long to wait.
Example request
curl -X DELETE https://api.aerscheduler.com/work-orders/:id/lines/:lineId \
  -H "Authorization: Bearer $AERSCHEDULER_KEY"