API reference contents
API reference
Documentation
The desk endpoints
The desk knows its own vocabulary. Read the composition rather than assuming a shape, then read what it is wired into, allowed to do, and watching — and ask it a question.
GET /desk
The composition of the screen: the blocks this particular desk is made of, in the business's own words. A logistics business and a music manager get different blocks, and neither is a variation on the other.
Do not assume the blocks. The names, the counts and the fields on a
desk belong to the business that owns it, and they change as the business's work
changes. A client that reads /desk can render any desk; a client that
hard-codes the four blocks it saw once will render one desk and lie about the rest.
curl -s "https://outcomedev.com/api/v1/desk" \
-H "Authorization: Bearer $OD_KEY"
{
"data": {
"sections": [
{
"id": "needs-you",
"title": "Needs you",
"blurb": "Work where the next move is yours.",
"kind": "work",
"kinds": ["renewal", "filing", "chase"],
"unit": null,
"fields": [],
"why": "Rows with an action of approve, answer or own, sorted the way the desk sorts."
},
{
"id": "sites",
"title": "Sites",
"blurb": "Every site the desk looks after and where it stands.",
"kind": "units",
"kinds": [],
"unit": "site",
"fields": [
{ "name": "reference", "hint": "The reference this business uses" },
{ "name": "contact", "hint": "Who to speak to" }
],
"why": "The unit this desk is built around."
},
{
"id": "money",
"title": "Money in and out",
"blurb": "What is owed, what is behind, what is due next.",
"kind": "money",
"kinds": [],
"unit": null,
"fields": [],
"why": "Installment plans running against the arrangements."
},
{
"id": "dates",
"title": "Dates that matter",
"blurb": "The next thirty days.",
"kind": "dates",
"kinds": [],
"unit": null,
"fields": [],
"why": "Everything falling due on the calendar."
}
]
}
}
| Field | Meaning |
|---|---|
id | The anchor for this block. Stable enough to store as a reference. |
title | The heading the person sees. |
blurb | One line saying what the block is for. |
kind | What sort of block it is: work, units, money or dates. This is the part that is an enum. |
kinds | For a work block: which work kinds it collects. This is the authoritative list of kinds on this desk — use it to build a filter rather than guessing values for /work?kind=. |
unit | For a units block: what one row is. This is the value to pass as /units?type=. |
fields | The extra columns this business records on its units, each with a name and a hint. |
why | Why the block exists. Written for a person, and the fastest way for an agent to understand what a block is for without a screenshot. |
The composition is also the vocabulary a person sees. Reading it before talking to somebody about their desk is the difference between saying "your filing" and saying the word they actually use.
The desk and its counts
GET /me is the summary call: the desk's record, plus three counts that answer
"how much is there" without fetching a page.
{
"data": {
"key": { "id": 4, "name": "reporting-script", "prefix": "od_K7fQ2mVx" },
"desk": { "id": "northgate", "name": "Northgate", "slug": "northgate", "plan": "growth", "status": "active" },
"work": { "open": 34, "waiting": 6, "closed_today": 3 }
}
}
| Count | Counts |
|---|---|
open | Everything not closed: open, in_progress and waiting. This is the same number open_total reports on /work. |
waiting | The subset where an answer is owed. A row can be open and not waiting. |
closed_today | Rows closed since midnight. |
GET / is the cheaper call and gives the desk id without the counts. Use it as
a key check; use /me when the counts are wanted.
GET /connections
What the desk is wired into. This is the single most useful call for explaining why nothing has gone out: with no mailbox connected, the desk prepares and records everything and nothing leaves.
curl -s "https://outcomedev.com/api/v1/connections" \
-H "Authorization: Bearer $OD_KEY"
{
"data": {
"items": [
{
"provider": "gmail",
"name": "Google Workspace",
"category": "mail",
"auth_type": "oauth",
"status": "connected",
"connected": true
},
{
"provider": "xero",
"name": "Xero",
"category": "accounting",
"auth_type": "oauth",
"status": "pending",
"connected": false
},
{
"provider": "storage",
"name": "Document storage",
"category": "files",
"auth_type": "token",
"status": "error",
"connected": false
}
]
}
}
| Field | Meaning |
|---|---|
provider | The integration's identifier. |
name | The name a person knows it by. |
category | What it is for: mail, accounting, files and others. |
auth_type | How it was authorised — oauth, token, or a login the desk drives. |
status | connected, pending, error, or something else the desk has decided. |
connected | Convenience: status === "connected". Use this rather than string-matching the status. |
Rows are ordered with problems first, then connected, then pending. Nothing here can be changed through the API: connecting a service is a signed-in action in the product.
GET /domains
The domains a business holds, their state, and who holds them — registrar, or a broker acting for the business — with a summary for a dashboard header.
{
"data": {
"items": [
{
"id": 3,
"name": "northgate.example",
"registrar": "Example Registrar",
"status": "live",
"renewal_on": "2027-02-11",
"amount_cents": 1400,
"currency": "GBP",
"broker_name": "Example Broker",
"notes": null
}
],
"summary": {
"total": 1,
"live": 1,
"owing": 0,
"owing_amount": 0,
"broker": "Example Broker",
"broker_configured": true,
"next": { "id": 3, "name": "northgate.example", "renewal_on": "2027-02-11" }
}
}
}
| Field | Meaning |
|---|---|
status | Where the domain stands. live is the normal state; unpaid and collections are the two that need attention. |
renewal_on | The date it renews, or null when nobody has recorded one. |
amount_cents | Minor units, with currency beside it. Never a float. |
summary.next | The soonest renewal on the desk, so a screen can lead with the one that matters. Null when no renewal dates are recorded. |
summary.owing_amount | The total of the domains that are unpaid or in collections. |
GET /permissions
What the desk may do without asking, and how much work each grant would cover. This is
the endpoint behind allowed on a work item, and it is the one to read before
telling a person what their desk can do on its own.
curl -s "https://outcomedev.com/api/v1/permissions" \
-H "Authorization: Bearer $OD_KEY"
{
"data": {
"classes": {
"reach_out": { "doing": 12, "asking": 4, "covered": 2 },
"chase": { "doing": 6, "asking": 0, "covered": 5 },
"file": { "doing": 0, "asking": 7, "covered": 0 },
"commit": { "doing": 0, "asking": 1, "covered": 0 },
"money": { "doing": 0, "asking": 2, "covered": 0 },
"sign": { "doing": 0, "asking": 1, "covered": 0 }
},
"unclassified": 0,
"needs_you": 15,
"with_desk": 18
}
}
| Field | Meaning |
|---|---|
classes | One entry per permission class, always all six, whether or not the desk has such work. |
doing | Open rows in that class the desk is allowed to act on. |
asking | Open rows in that class waiting on a person's decision. |
covered | Rows already out with somebody else. Nobody is being asked for anything, so they are neither doing nor asking. |
unclassified | Open rows the desk does not recognise as any of the six classes. These always ask, whatever else is granted. |
needs_you | The sum of every asking — how much work is the person's. |
with_desk | The sum of every doing — how much the desk has in hand. The two add up to the open queue. |
The six classes are reach_out, chase, file,
commit, money and sign. They are verbs rather than
nouns, and they are the same six whatever the business does, because a permission
vocabulary that changes per business cannot be reasoned about by the person granting it.
money and sign are locked. Not because a
customer might not want to grant them, but because acting on them is a different kind of
act, and a confirmation click has never been an adequate substitute for the thing
itself. The desk prepares those and a person performs them. Their doing
count is therefore always 0, and a work item in either class always reports
allowed: false.
There is no endpoint to change a grant. It is made by the owner in the product, and
that is what makes POST /work/{id}/run a gate rather than a formality.
GET /watches
The dates and counts the desk keeps an eye on, per kind of thing. A watch is why a desk knows a date is coming, and it is defined by the business rather than compiled into the product.
{
"data": {
"items": [
{
"id": 14,
"subject_type": "site",
"field": "certificate_expires_on",
"label": "Certificate expiry",
"lead_days": 60,
"kind": "renewal",
"action": "reach_out",
"every": null,
"priority": 70
},
{
"id": 15,
"subject_type": "site",
"field": "last_inspection_on",
"label": "Inspection due",
"lead_days": 30,
"kind": "inspection",
"action": "reach_out",
"every": "365 days",
"priority": 60
}
]
}
}
| Field | Meaning |
|---|---|
subject_type | The kind of thing being watched, matching /units?type=. |
field | Which field on that thing holds the date. |
label | The name a person sees. |
lead_days | How far ahead the desk starts working it. |
kind | What sort of work it turns into, so it lands under a /work?kind= filter. |
action | Which permission class the resulting work falls in. |
every | Set for something recurring rather than one-off. Null otherwise. |
priority | The priority the resulting rows start at. |
Reading watches is the best way to answer "why is this row here?" without asking anybody: a watch, a lead time and a date on a unit explain most rows on a desk.
POST /chat
The same conversation a person has in the product's chat box. Useful when the question is not answerable by reading an endpoint — "what is outstanding on the Ash Lane site", "what should I do about the Calderway contract".
curl -s -X POST "https://outcomedev.com/api/v1/chat" \
-H "Authorization: Bearer $OD_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: chat-2026-10-13-1" \
--max-time 300 \
-d '{"text":"What is outstanding on the Ash Lane site?"}'
{
"data": {
"question": "What is outstanding on the Ash Lane site?",
"answer": "Two things. The certificate expiry is 4 November and the renewal has not been started. The second instalment is 20 days behind and the desk has already chased once.",
"timed_out": false,
"desk_added": []
}
}
| Field | Meaning |
|---|---|
question | The text the desk took as the question, trimmed. |
answer | The desk's reply, or null if it timed out. |
timed_out | True when the desk did not answer in time. This is not an error status: the HTTP response is still 200, and nothing was changed. |
desk_added | Any block the desk added to the screen because the question warranted one. Already saved — the array is a report, not a proposal to approve. |
Slow. A session answers this, so allow up to four minutes. Set your
client timeout accordingly; a default of thirty seconds will time out on almost every
call that is not trivially answerable. Treat a client-side timeout as unknown rather
than failed, and retry with the same Idempotency-Key.
Empty text is refused. 400 bad_request with "Say something
in text."
desk_added is the interesting part for an agent. Asking for something to be
put on the desk produces a real block, in the same way the chat box in the product does,
and the agent ends up with the same desk a person would have made.
GET /activity and GET /events
Two records, and the distinction matters. /activity is everything written or
sent: messages, notes, and what the desk wrote back after a run.
/events is the desk's own log: what it did and when, including API writes
themselves.
curl -s "https://outcomedev.com/api/v1/activity?limit=2" \
-H "Authorization: Bearer $OD_KEY"
{
"data": {
"items": [
{
"id": 219,
"obligation_id": 58,
"outcome_id": null,
"channel": "desk",
"direction": "outbound",
"subject": "Run report",
"body": "Submitted the quarterly return for the period ending 30 September 2026.",
"status": "sent",
"actor": "desk",
"created_at": "2026-10-13T07:22:41.006Z",
"sent_at": "2026-10-13T07:22:41.006Z",
"approved_at": null
}
]
}
}
Both endpoints take limit only. They have no cursor, and both
return the newest entries first — the opposite of /work, which returns
the desk's working order. Neither reports a total.
{
"data": {
"items": [
{
"id": 902,
"kind": "api_write",
"message": "POST /work/47/complete",
"data": { "key": "od_K7fQ2mVx" },
"created_at": "2026-10-13T07:24:10.551Z"
}
]
}
}
The log is where a client's own writes show up, named by the key that made them. If a write appears twice with different values, the key was changed between the attempts.
Related
- Work — how
allowedandpermissionon a row relate to the grants here. - Arrangements — the units these blocks count.
- Errors — and the
no_deskcase, which comes from the desk rather than the key.