Skip to content
WorkOrders

For developers

Work that starts somewhere else

A building management system. A tenant portal. Your own maintenance schedule. Send the request to one endpoint and it becomes a work order like any other. It goes into your jobs list and out to your team, and the customer gets the usual updates.

What it's for

Most work arrives as email. Some of it doesn't.

If your tenants log faults in a portal, the request is already in a database, with a site and a reference. Nobody should have to retype it into an email just so WorkOrders can read it back out.

Raise it

One POST with a subject and a description. You get back a WO- reference to store against your own record, and the job is already in the list by the time the response reaches you.

Follow it

Read the job back, or read its timeline: who took it, when they went on site, what they found and when it was completed. It's the same history the office sees.

Attach to it

Photos, video and PDFs. Send small files inline with the job so they arrive together. Post larger ones afterwards, up to 100 MB each.

A job raised this way is handled exactly like an emailed one. The AI reads it the same way, so a site address buried in a paragraph still gets picked out. It sends the same notifications, appears in the same list, and can be searched and reported on the same way. It also shows up in the customer's own portal, and their replies are added to it.

Getting a key

Keys belong to the company

Nobody is signed in when your server calls us, so the key is issued to the business instead of to a member of staff. It keeps working if the person who set it up leaves.

An owner or administrator creates one under Settings → API keys. Give it a name that says what it's for (this is shown on every job it raises), choose full access or read-only, and optionally set an expiry date.

The key is shown once and can't be recovered. We only store a hash of it, so there's no way to show it to you again. If you lose a key, create a new one and revoke the old one. Do the same if a key leaks.

Revoking takes effect on the next request. Keys start with wo_ so you can spot one in a config file, a log, or a commit that shouldn't have contained one.

Use read-only keys where you can

If what you're building only displays jobs, like a dashboard or a status page for tenants, give it a read-only key. Then it can't raise jobs or add notes, whatever the code does.

work-orders:read
List jobs, read one, read its timeline.
work-orders:write
Raise a job, add a note, upload a file.
*
Both. What the Full access option grants.

Authentication

One header, and one call to test it

Every request carries Authorization: Bearer wo_…. Never put the key in the query string. URLs end up in proxy logs and browser history, and the key would end up there too.

Run this first

curl -s https://workorders.nz/api/v1/integration/ping \
  -H 'Accept: application/json' \
  -H "Authorization: Bearer $WORKORDERS_KEY"

Run it before you write any code. It tells you whether the key works, which company's account it belongs to, and what it's allowed to do. It's worth checking the account before you post a job, in case you've pasted the wrong key.

What comes back

{
  "data": {
    "company": { "id": 1, "name": "Demo Plumbing & Gas" },
    "token": {
      "name": "Acme FM platform",
      "abilities": ["*"],
      "expires_at": null
    },
    "server_time": "2026-08-06T22:15:55Z"
  }
}

server_time is our clock. Every timestamp we send is UTC with a trailing Z. If your server's clock is an hour out, it's better to find out here than from jobs filed at the wrong time.

Raising a job

One required field

Only the subject is required. Everything else is optional, so a one-line fault report is enough to raise a job.

POST /api/v1/integration/work-orders

curl -s -X POST \
  https://workorders.nz/api/v1/integration/work-orders \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $WORKORDERS_KEY" \
  -H 'Idempotency-Key: fm-99213-2026-08-07' \
  -d '{
    "subject": "No hot water, Level 3 kitchen",
    "body": "Reported by the duty manager. The cylinder at 12 Bond Street is cold and there is water on the floor.",
    "requester_name": "Dana Whitfield",
    "requester_email": "dana@acmefm.example",
    "site_address": "12 Bond Street, Wellington",
    "urgency": "high",
    "external_reference": "FM-99213"
  }'

201 Created

{
  "data": {
    "id": 1,
    "reference": "WO-P6J9CE",
    "status": "new",
    "status_label": "New",
    "source": "api",
    "urgency": "high",
    "subject": "No hot water, Level 3 kitchen",
    "summary": null,
    "from": {
      "name": "Dana Whitfield",
      "email": "dana@acmefm.example"
    },
    "site_address": "12 Bond Street, Wellington",
    "assignee": null,
    "created_at": "2026-08-06T22:15:55Z",
    "updated_at": "2026-08-06T22:15:55Z",
    "attachments": []
  }
}

Shortened. The real response also includes the customer, the site and the rest of the job's fields. Store the reference. Every other endpoint uses it, and it's what the customer reads out over the phone. The summary, and the urgency if you didn't send one, are filled in a few seconds later, once the AI has read the description.

The fields

subject required
What's wrong, in one line. Up to 512 characters.
body optional
The description, up to 20,000 characters. You can paste a whole email thread into it. This is what the AI reads to set the urgency and find the site.
requester_name optional
Who reported it: a tenant, a duty manager.
requester_email optional
Who reported it. We find or create a customer record from this, and it's where status emails go.
site_address optional
Where, as written. Kept exactly as sent.
urgency optional
low, normal, high or emergency. Leave it out and the AI sets one from the description.
external_reference optional
Your own id for the job. We keep it alongside ours, so either side can look the job up when someone rings.
attachments optional
Up to 10 files inline, base64-encoded, 5 MB each and 15 MB per request.

Send an Idempotency-Key

If you post a job and never hear back, you don't know whether we created it. You should retry, but without a key, retrying is how one burst pipe becomes three jobs.

Put any string you like in the Idempotency-Key header. Your own job id is usually right. Retry the identical request and you get the original response back, marked Idempotent-Replay: true, instead of a second job.

If you reuse the same key for a different job, you get a 409. Otherwise we'd lose the new fault report and tell you we'd created a job that doesn't exist.

The header is optional, and we won't refuse a job without it. But please send it.

Following it

All seven endpoints

Base URL https://workorders.nz/api/v1/integration. Everything is JSON.

GET ping
Test the key. It needs no permissions, so a read-only key can use it too.
POST work-orders
Raise a job.
GET work-orders
List jobs, newest first. Filter by status, assignee, customer, date range or free text. 25 per page by default, up to 100.
GET work-orders/{reference}
One job, with its full timeline.
GET work-orders/{reference}/events
Just the timeline, oldest first. Poll this to keep track of progress. Unlike the status, it shows who did what.
POST work-orders/{reference}/notes
Add to the history without changing the status. "Tenant says it has got worse."
POST work-orders/{reference}/attachments
Upload one file, up to 100 MB.

References are forgiving

WO-P6J9CE, wo-p6j9ce and p6j9ce all find the same job. If you stored it in lower case, or dropped the prefix, you'll still find the right job.

Polling, not webhooks

There is nothing pushed to you yet. Ask for ?updated_since= with the newest updated_at you've seen, and you'll get everything that has changed since. The time you give is included, so you may see a job twice, but you'll never miss one.

What the API won't do

You can't assign, complete or reopen a job through this API, and we don't plan to add that.

Each of those changes is recorded against a named person (who took the job, who was on site, who signed it off), and a company key isn't a person. People rely on that history when something goes wrong on a site, so it has to be accurate.

Add a note instead. It shows on the job in the office and on the technician's phone, marked as coming from your system.

When it goes wrong

Standard status codes, with a plain-English message

401
The key is missing, wrong, revoked or expired. All four give the same response, so a stranger can't tell whether a key they've found was ever real.
403
The key works but doesn't have permission for this. The response names the permission it needs, so you can create a key that has it.
404
There's no such job on your account. A job belonging to another company also returns 404, so you can't tell whether it exists.
409
An Idempotency-Key conflict. Either the key was used for a different job, or the first request hasn't finished yet, so there's nothing to replay.
422
The request was refused. The response includes an errors object that says, for each field, what's wrong with it.
429
Too fast. Wait for Retry-After.

A 422, in full

{
  "message": "The subject field is required.",
  "errors": {
    "subject": ["The subject field is required."]
  }
}

Rate limits

120 requests a minute per key. It's counted per key, not per IP address, so someone else behind the same corporate firewall can't use up your limit. Every response includes X-RateLimit-Remaining so you can see how many you have left.

The limit is there to stop a runaway loop. If you really need more, ask us.

Generate a client

The full API, machine-readable

An OpenAPI 3.1 document covering every endpoint, field, error and example on this page. Point your code generator at it to get a typed client in whatever language you're using.

Our test suite checks it. A build fails if the spec describes an endpoint we don't serve, or if we add one it doesn't mention, so it stays up to date.

openapi: 3.1.0
info:
  title: WorkOrders Intake API
  version: '1.0'
servers:
  - url: https://workorders.nz/api/v1/integration
paths:
  /ping: …
  /work-orders: …
  /work-orders/{reference}: …

/developers/openapi.yaml

Try it against your own system

Start a trial, create a key in Settings, and post a job from the system your work starts in. There's nothing to sign and no sales demo to sit through.