Home › Documentation › API › The desk

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."
      }
    ]
  }
}
FieldMeaning
idThe anchor for this block. Stable enough to store as a reference.
titleThe heading the person sees.
blurbOne line saying what the block is for.
kindWhat sort of block it is: work, units, money or dates. This is the part that is an enum.
kindsFor 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=.
unitFor a units block: what one row is. This is the value to pass as /units?type=.
fieldsThe extra columns this business records on its units, each with a name and a hint.
whyWhy 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 }
  }
}
CountCounts
openEverything not closed: open, in_progress and waiting. This is the same number open_total reports on /work.
waitingThe subset where an answer is owed. A row can be open and not waiting.
closed_todayRows 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
      }
    ]
  }
}
FieldMeaning
providerThe integration's identifier.
nameThe name a person knows it by.
categoryWhat it is for: mail, accounting, files and others.
auth_typeHow it was authorised — oauth, token, or a login the desk drives.
statusconnected, pending, error, or something else the desk has decided.
connectedConvenience: 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" }
    }
  }
}
FieldMeaning
statusWhere the domain stands. live is the normal state; unpaid and collections are the two that need attention.
renewal_onThe date it renews, or null when nobody has recorded one.
amount_centsMinor units, with currency beside it. Never a float.
summary.nextThe soonest renewal on the desk, so a screen can lead with the one that matters. Null when no renewal dates are recorded.
summary.owing_amountThe 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
  }
}
FieldMeaning
classesOne entry per permission class, always all six, whether or not the desk has such work.
doingOpen rows in that class the desk is allowed to act on.
askingOpen rows in that class waiting on a person's decision.
coveredRows already out with somebody else. Nobody is being asked for anything, so they are neither doing nor asking.
unclassifiedOpen rows the desk does not recognise as any of the six classes. These always ask, whatever else is granted.
needs_youThe sum of every asking — how much work is the person's.
with_deskThe 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
      }
    ]
  }
}
FieldMeaning
subject_typeThe kind of thing being watched, matching /units?type=.
fieldWhich field on that thing holds the date.
labelThe name a person sees.
lead_daysHow far ahead the desk starts working it.
kindWhat sort of work it turns into, so it lands under a /work?kind= filter.
actionWhich permission class the resulting work falls in.
everySet for something recurring rather than one-off. Null otherwise.
priorityThe 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": []
  }
}
FieldMeaning
questionThe text the desk took as the question, trimmed.
answerThe desk's reply, or null if it timed out.
timed_outTrue 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_addedAny 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.

  • Work — how allowed and permission on a row relate to the grants here.
  • Arrangements — the units these blocks count.
  • Errors — and the no_desk case, which comes from the desk rather than the key.