Skip to content

Work order lifecycle

Work orders are the one resource three different audiences touch: staff manage them, tenants report and follow them, and vendors do the work. The visibility rules matter as much as the state machine.

open ──> in_progress ──> completed
│ │
└────────────┴────────> cancelled

completed and cancelled are terminal. Any other transition returns 409:

{ "message": "Invalid status transition from completed to open" }

Staff must name a unit:

Terminal window
curl -X POST https://api.getvespy.com/api/work-orders \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"title": "Kitchen sink leaking under cabinet",
"description": "Standing water in the base cabinet, tenant has shut off the supply valve.",
"propertyId": "'"$PROPERTY_ID"'",
"unitId": "'"$UNIT_ID"'",
"priority": "high",
"category": "plumbing"
}'

priority is low, normal, high, or emergency.

When a tenant calls the same endpoint, the API overrides most of the body: source becomes tenant, and the property, unit, and tenant are taken from the caller’s active lease. A tenant cannot open a work order against a unit they do not rent, and cannot pre-assign a vendor.

If the unit has accessInstructions, they are copied onto the work order as entryInstructions — so whoever is dispatched gets the lockbox code without anyone looking it up.

Terminal window
curl -X POST "https://api.getvespy.com/api/work-orders/$WORK_ORDER_ID/assign" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"assignedVendorId":"'"$VENDOR_ID"'"}'

Assign a vendor, a staff user, or both. Pass null to clear one. Assignment does not change status — move it to in_progress separately:

Terminal window
curl -X POST "https://api.getvespy.com/api/work-orders/$WORK_ORDER_ID/status" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"status":"in_progress"}'

Assigning a vendor also lets them see the job in the vendor portal. If they do not have a login yet, send a vendor invite first.

Notes carry the conversation, and every note declares its audience:

visibility Visible to
organization_only Staff only
tenant_shared Staff and the tenant
vendor_shared Staff and the assigned vendor
tenant_vendor_shared Everyone on the job
Terminal window
curl -X POST "https://api.getvespy.com/api/work-orders/$WORK_ORDER_ID/notes" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"visibility":"tenant_shared","body":"Plumber scheduled for Thursday between 9 and 11am."}'

A tenant posting a note may only choose a visibility that includes tenants; anything else returns 403.

Expenses link to a work order through workOrderId, and GET /api/work-orders/{id} returns them as linkedExpenses with the notes and attachments. That detail response is the single call to make when rendering a work order page.

Terminal window
curl -X POST "https://api.getvespy.com/api/work-orders/$WORK_ORDER_ID/status" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"status":"completed"}'

Completion stamps completedAt and emits an event that downstream notifications consume. Because it is terminal, reopening means creating a new work order.