Home › Documentation › API › Actions

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.

EndpointBodyWhat it is for
POST /work/{id}/complete{ note?, actor? }Do the thing the row is asking for
POST /work/{id}/snooze{ days } 1–365Push the date out
POST /work/{id}/draftnoneHave the desk write the message
POST /work/{id}/runnoneHave 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

  1. Finds the newest message on the row still in the drafted state, if there is one.
  2. Sends it through the owner's own connected mailbox.
  3. Marks the message sent only if a provider accepted it. Anything else — no mailbox, a refusal, a network that did not answer — leaves it approved with the reason written down.
  4. Moves the row to waiting if the row is waiting on somebody else, and to done otherwise.
  5. 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 but done as 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 stays open with failed: true and the reason in delivery_note, and last_error is set on the row so it arrives on the desk as an exception rather than as a success. Check delivered, not just the HTTP status.
  • 400 bad_request when the body is not JSON the route can read. 404 not_found when 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_request with "days must be between 1 and 365." The range is enforced, not clamped. A client sending 0 or 400 gets a refusal rather than a surprise. Omitting days entirely 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_found when 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_found when 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_permitted with details.permission naming the class that would be needed. Full body in Errors.
  • ok: false with a reason when 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_out appears on a failed run to distinguish "the desk ran out of time" from "the desk tried and reported a problem".
  • Overlapping runs. A second run on 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_request with "Say something in body." An empty or whitespace-only body is refused. This is how a person learns the note did not save.
  • 400 bad_request when the body is missing entirely or is not JSON.
  • 404 not_found when the arrangement is not on this desk. No note is written.
  • channel is not validated beyond call. Anything other than the literal call is stored as note. 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.

  • Work — the row these endpoints act on.
  • Errors — the full 403 and 400 bodies.
  • Idempotency — retrying a write without doing it twice.