API reference contents
API reference
Documentation
Write endpoints
Five writes: complete a row, snooze it, prepare its message, have the desk carry it out, or write a note against an arrangement. Each one is idempotent when given a key.
Every write in this API takes an Idempotency-Key header and returns
{ "data": ... }. All of them are safe to retry with that header, which is
covered in Idempotency. Two of them have
outcomes that look like failures and are not.
| Endpoint | Body | What it is for |
|---|---|---|
POST /work/{id}/complete | { note?, actor? } | Do the thing the row is asking for |
POST /work/{id}/snooze | { days } 1–365 | Push the date out |
POST /work/{id}/draft | none | Have the desk write the message |
POST /work/{id}/run | none | Have the desk carry the work out itself |
POST /arrangements/{id}/notes | { body, actor?, channel? } | Write down what was learned |
POST /work/{id}/complete
When to use it. When the row's action.type is
approve, and the prepared message has been read and is right. Completing
sends that message on the owner's behalf. This is the endpoint the Approve and send
button calls. It is also the right call for a row that a person did by hand and wants
recorded — in that case there is no draft, and completing writes the note and closes
the row.
curl -s -X POST "https://outcomedev.com/api/v1/work/47/complete" \
-H "Authorization: Bearer $OD_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: complete-47-2026-10-13" \
-d '{"note":"Approved by phone, reference 88421.","actor":"nightly-script"}'
Both fields are optional. actor defaults to api and appears in the record as whoever did it.
{
"data": {
"id": 47,
"status": "waiting",
"waiting_on": "the council",
"failed": false,
"delivered": true,
"delivery_note": "Sent from practice@northgate.example."
}
}
What it does, exactly
- Finds the newest message on the row still in the
draftedstate, if there is one. - Sends it through the owner's own connected mailbox.
- Marks the message
sentonly if a provider accepted it. Anything else — no mailbox, a refusal, a network that did not answer — leaves itapprovedwith the reason written down. - Moves the row to
waitingif the row is waiting on somebody else, and todoneotherwise. - Appends the note to the row's
detail, if a note was given.
What can go wrong
-
status: "waiting"is success. A row that is waiting on somebody else does not close, because a message going out is not the same as the job being finished. A client that treats anything butdoneas a failure will re-complete a row that is already out. -
delivered: false. The desk meant to send and could not. When that happens the row staysopenwithfailed: trueand the reason indelivery_note, andlast_erroris set on the row so it arrives on the desk as an exception rather than as a success. Checkdelivered, not just the HTTP status. -
400 bad_requestwhen the body is not JSON the route can read.404 not_foundwhen the id does not exist on this desk — note that a row belonging to another desk is indistinguishable from a row that does not exist, and that is intentional.
POST /work/{id}/snooze
When to use it. When the work is real but this is not the moment. It is available on every row, and it is the right answer more often than it looks: a chase deferred by a week is better than one sent on a day nobody will read it.
curl -s -X POST "https://outcomedev.com/api/v1/work/47/snooze" \
-H "Authorization: Bearer $OD_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: snooze-47-2026-10-13" \
-d '{"days":7}'
{
"data": {
"id": 47,
"title": "Renew the business licence",
"kind": "renewal",
"status": "open",
"due_date": "2027-03-07",
"priority": 87
}
}
What it does, exactly
Moves the due date forward by days from today, or from the existing date if
there is one, and reduces the row's priority by 5 points, never below 0. It writes an
entry to the desk's own log. The row stays exactly as open as it was.
What can go wrong
-
400 bad_requestwith "days must be between 1 and 365." The range is enforced, not clamped. A client sending0or400gets a refusal rather than a surprise. Omittingdaysentirely is accepted and means 3. -
Snoozing is not completing. It does not send anything, and it does not
change
action. A row that was asking for approval is still asking for approval on the new date. -
404 not_foundwhen the id is not on this desk.
POST /work/{id}/draft
When to use it. When action.type is prepare.
This asks the desk to write the message for the row, and it is the step between a row
nobody has touched and a row a person can approve.
curl -s -X POST "https://outcomedev.com/api/v1/work/58/draft" \
-H "Authorization: Bearer $OD_KEY" \
-H "Idempotency-Key: draft-58-2026-10-13"
{
"data": {
"drafted": true,
"channel": "email",
"subject": "Quarterly return - Northgate",
"body": "Hello,\\n\\nThe quarterly return for Northgate is due on 30 November 2026. Could you confirm what is outstanding and the reference to quote?\\n\\nThank you.",
"to": "records@example.gov"
}
}
What it does, exactly
Guarantees a draft exists. The desk never re-drafts the same row on every call, so this is idempotent in its own right as well as through the header: a row that already has a prepared message returns the one it has, unchanged. The response gives the channel, the subject, the body and the recipient, so a client can show the message before anybody approves it.
drafted: false with a reason is a success, not an
error. The desk declines to write a message when there is nothing true to say
— usually because there is nobody on the row to write to yet. A client that treats
this as a failure will retry for ever, because the condition will not change on its own.
{
"data": {
"drafted": false,
"reason": "Nothing is prepared for this one - there is nobody on the row to write to, or nothing true to say yet. Say who it goes to, or that you handle it yourself."
}
}
The HTTP status is 200. The reason is written for a person to
read and can be shown as-is. The right response to it is to ask the person who this goes
to, not to call the endpoint again.
What can go wrong
404 not_foundwhen the id is not on this desk. This is the only error this endpoint raises in normal use: a refusal to draft is reported in the body, not as a status.
POST /work/{id}/run
When to use it. When action.type is run and the
work should happen now rather than on the desk's own schedule. The desk drives a browser
or a portal on the owner's behalf — signing in, filling a form, submitting it,
reading what came back.
curl -s -X POST "https://outcomedev.com/api/v1/work/58/run" \
-H "Authorization: Bearer $OD_KEY" \
-H "Idempotency-Key: run-58-2026-10-13" \
--max-time 900
{
"data": {
"ok": true,
"closed": true,
"outcome": "Filed the quarterly return and saved the acknowledgement.",
"needs": null,
"waiting": null,
"done": "Submitted the quarterly return for the period ending 30 September 2026.",
"result": "Acknowledgement 4471-220-A, saved to the desk's record.",
"activity_id": 219,
"ms": 214600
}
}
What it does, exactly
Reads the row, works it, and writes a report back onto the record as an activity. A
successful run that finished the work closes the row. A run that could not finish leaves
the row open and reports what is missing in needs or who is being waited on
in waiting. ms is how long it actually took.
run returns 403 not_permitted unless the desk was
granted that class of work — the same gate the button on the screen sits
behind, and the same check as the allowed field on the row. If
allowed is false, this call will be refused. The grant is made
by the owner in the desk's permissions, through the product, and there is no header or
body that substitutes for it.
It takes minutes, not milliseconds. A run drives a real site. Set a
client timeout of at least fifteen minutes and do not retry on timeout without an
Idempotency-Key, or the work will be attempted twice.
What can go wrong
403 not_permittedwithdetails.permissionnaming the class that would be needed. Full body in Errors.ok: falsewith areasonwhen the row is marked as needing a person, or when the desk did not finish in time. Nothing was changed unless the report says so.timed_outappears on a failed run to distinguish "the desk ran out of time" from "the desk tried and reported a problem".- Overlapping runs. A second
runon the same row while the first is in flight is a second attempt at the same work. Use an idempotency key of the same value and the second call returns the first response.
POST /arrangements/{id}/notes
When to use it. When a person learned something the desk does not know: what was agreed in a phone call, what came out of a site visit, a reference number read out loud. Arrangements are the long-running relationships, and their notes are how unstructured knowledge gets into the record. Full detail is in Arrangements.
curl -s -X POST "https://outcomedev.com/api/v1/arrangements/12/notes" \
-H "Authorization: Bearer $OD_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: note-12-2026-10-13" \
-d '{"body":"Called the agent. They will confirm the certificate by Friday.","actor":"sam","channel":"call"}'
What it does, exactly
Writes the note against the arrangement, with channel defaulting to
note and actor defaulting to api. The desk reads
these when it works the arrangement, which is the whole point: a note only a person can
see is a note the desk contradicts later. The body is trimmed and truncated at 4,000
characters.
What can go wrong
400 bad_requestwith "Say something inbody." An empty or whitespace-only body is refused. This is how a person learns the note did not save.400 bad_requestwhen the body is missing entirely or is not JSON.404 not_foundwhen the arrangement is not on this desk. No note is written.channelis not validated beyondcall. Anything other than the literalcallis stored asnote. A typo does not error — it silently becomes a note, so send the two values exactly.
Things with no write endpoint
Worth knowing before designing around an absence. There is no route to create, edit or
close a work item, no route to change a permission grant, and no route to add a
connection or a watch. Those are product actions taken by the owner, and version 1 of the
API reads them rather than setting them. POST /chat is a partial exception:
it can ask the desk to add a block to the screen, and it reports back what it added.
Related
- Work — the row these endpoints act on.
- Errors — the full 403 and 400 bodies.
- Idempotency — retrying a write without doing it twice.