Home › Documentation › API › Code examples

Code examples

Four clients, the same four jobs: authenticate, list work, find something needing approval, and complete it with a note. Working code, in full.

Each program below does exactly the same thing in the same order, so they can be read against each other. Each one reads the key from OUTCOMEDEV_API_KEY, lists the work, picks the first row whose action is approve, reads it to show the message that is about to go out, and completes it with a note and an idempotency key.

Every one of these sends a real message. The final step on a row whose action is approve releases the prepared message to a real counterparty, through the owner's own mailbox. Run these against a desk where that is acceptable, or stop before the last call while reading them.

Before any of them

export OUTCOMEDEV_API_KEY=od_K7fQ2mVx8sT1pR4nB6wY3zA

The key comes from the account menu inside the product, as described in Authentication. It is read from the environment in every example, because a key in a source file is a key in a repository.

curl

No build step, and the fastest way to check the shape of anything. The four calls are separate here because that is how they are useful interactively.

#!/usr/bin/env bash
set -euo pipefail

API="https://outcomedev.com/api/v1"
AUTH="Authorization: Bearer ${OUTCOMEDEV_API_KEY:?set OUTCOMEDEV_API_KEY first}"
JSON="Content-Type: application/json"

# 1. Authenticate. The root confirms the key and names the desk.
curl -sS "$API/" -H "$AUTH"
echo

# 2. List work. Only what is still open.
curl -sS "$API/work?status=open&limit=50" -H "$AUTH" \
  | tee /tmp/od-work.json
echo

# 3. Find something needing approval.
ID=$(jq -r '.data.items[] | select(.action.type == "approve") | .id' /tmp/od-work.json | head -1)

if [ -z "$ID" ]; then
  echo "Nothing is waiting for approval."
  exit 0
fi

echo "Work item $ID needs approval."

# Read it first, so the message going out is the message that was meant.
curl -sS "$API/work/$ID" -H "$AUTH" \
  | jq '.data.messages[] | select(.status == "drafted") | { subject, body, to: .actor }'

# 4. Complete it, with a note and an idempotency key.
curl -sS -X POST "$API/work/$ID/complete" \
  -H "$AUTH" -H "$JSON" \
  -H "Idempotency-Key: complete-$ID-$(date -u +%Y-%m-%d)" \
  -d '{"note":"Reviewed and approved.","actor":"examples-curl"}' \
  | jq .

Output, on a desk with three open rows one of which is waiting for approval:

{
  "api": "OutcomeDev",
  "version": "v1",
  "desk": "northgate",
  "key": { "id": 4, "name": "examples-curl", "prefix": "od_K7fQ2mVx" },
  "documentation": "https://outcomedev.com/docs/api/",
  "openapi": "/api/v1/openapi.json",
  "note": "Each key acts as one desk. Writes here are the same actions a person takes on the screen, and anything that sends a message goes out on the owner's behalf after they approved it."
}
...
Work item 47 needs approval.
{
  "subject": "Business licence renewal - Northgate",
  "body": "Hello,\n\nThe business licence for Northgate is due for renewal on 28 February 2027. Could you confirm the renewal fee and the documents needed?\n\nThank you.",
  "to": "desk"
}
{
  "data": {
    "id": 47,
    "status": "waiting",
    "waiting_on": "the council",
    "failed": false,
    "delivered": true,
    "delivery_note": "Sent from practice@northgate.example."
  }
}

The response to the first call is printed without the data wrapper because jq . was not applied to it — the raw bytes include it. A client should always read .data.

JavaScript

Node 18 or later, no dependencies. Uses the built-in fetch.

// node --env-file=.env examples.mjs
const BASE = 'https://outcomedev.com/api/v1'

const key = process.env.OUTCOMEDEV_API_KEY
if (!key) throw new Error('Set OUTCOMEDEV_API_KEY first.')

const headers = { Authorization: `Bearer ${key}` }

/** Every response is wrapped: read data on success, error on failure. */
async function call(path, init = {}) {
  const res = await fetch(`${BASE}${path}`, {
    ...init,
    headers: {
      ...headers,
      ...(init.body ? { 'Content-Type': 'application/json' } : {}),
      ...init.headers,
    },
  })

  const body = await res.json().catch(() => null)

  if (!res.ok) {
    const err = body && body.error ? body.error : { code: 'unknown', message: `HTTP ${res.status}` }
    const e = new Error(err.message)
    e.code = err.code
    e.status = res.status
    e.details = err.details
    throw e
  }

  return body.data
}

// 1. Authenticate.
const root = await call('/')
console.log(`Connected to the ${root.desk} desk as "${root.key.name}".`)

// 2. List work.
const page = await call('/work?status=open&limit=50')
console.log(`${page.open_total} open, ${page.items.length} on this page.`)

// 3. Find something needing approval.
const row = page.items.find((item) => item.action && item.action.type === 'approve')

if (!row) {
  console.log('Nothing is waiting for approval.')
} else {
  console.log(`#${row.id} ${row.title} - ${row.action.label}`)

  // Read it first: completing sends the prepared message.
  const full = await call(`/work/${row.id}`)
  const draft = full.messages.find((m) => m.status === 'drafted')

  if (draft) {
    console.log('--- about to go out ---')
    console.log(`Subject: ${draft.subject}`)
    console.log(draft.body)
    console.log('-----------------------')
  }

  // 4. Complete it. The same key on a retry returns this response, not a second send.
  const done = await call(`/work/${row.id}/complete`, {
    method: 'POST',
    headers: { 'Idempotency-Key': `complete-${row.id}-${new Date().toISOString().slice(0, 10)}` },
    body: JSON.stringify({ note: 'Reviewed and approved.', actor: 'examples-node' }),
  })

  console.log(`#${done.id} is now ${done.status}.`)
  console.log(done.delivery_note)

  if (done.failed) {
    // The desk meant to send and could not. The row is open again with the reason attached.
    process.exitCode = 1
  }
}

Note the two behaviours a client has to get right, and both are in this file. A non-2xx response is turned into an error carrying code, so a caller can branch on not_permitted rather than on a status. And failed is checked after a successful response, because a 200 can still mean the message did not go.

Python

Python 3.8 or later, with requests.

"""python examples.py"""
import os
import datetime
import requests

BASE = "https://outcomedev.com/api/v1"

KEY = os.environ.get("OUTCOMEDEV_API_KEY")
if not KEY:
    raise SystemExit("Set OUTCOMEDEV_API_KEY first.")

SESSION = requests.Session()
SESSION.headers["Authorization"] = f"Bearer {KEY}"


def call(path, method="GET", **kwargs):
    """Read data on success, raise on failure with the API's own code."""
    res = SESSION.request(method, BASE + path, timeout=60, **kwargs)

    try:
        body = res.json()
    except ValueError:
        body = None

    if not res.ok:
        err = (body or {}).get("error", {})
        raise RuntimeError(f"{err.get('code', 'unknown')}: {err.get('message', res.status_code)}")

    return body["data"]


# 1. Authenticate.
root = call("/")
print(f"Connected to the {root['desk']} desk as \"{root['key']['name']}\".")

# 2. List work.
page = call("/work", params={"status": "open", "limit": 50})
print(f"{page['open_total']} open, {len(page['items'])} on this page.")

# 3. Find something needing approval.
row = next(
    (i for i in page["items"] if i.get("action") and i["action"]["type"] == "approve"),
    None,
)

if row is None:
    print("Nothing is waiting for approval.")
else:
    print(f"#{row['id']} {row['title']} - {row['action']['label']}")

    # Read it first: completing sends the prepared message.
    full = call(f"/work/{row['id']}")
    draft = next((m for m in full["messages"] if m["status"] == "drafted"), None)

    if draft:
        print("--- about to go out ---")
        print(f"Subject: {draft['subject']}")
        print(draft["body"])
        print("-----------------------")

    # 4. Complete it. The same key on a retry returns this response, not a second send.
    today = datetime.date.today().isoformat()
    done = call(
        f"/work/{row['id']}/complete",
        method="POST",
        headers={"Idempotency-Key": f"complete-{row['id']}-{today}"},
        json={"note": "Reviewed and approved.", "actor": "examples-python"},
    )

    print(f"#{done['id']} is now {done['status']}.")
    print(done["delivery_note"])

    if done["failed"]:
        # The desk meant to send and could not. The row is open again with the reason attached.
        raise SystemExit(1)

The timeout on the session is 60 seconds, which is right for everything here except /chat and /run. Those two need minutes; pass a longer timeout for them specifically rather than raising it for every call.

Go

Standard library only. Two structs describe what this program reads.

// go run examples.go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"time"
)

const base = "https://outcomedev.com/api/v1"

var httpClient = &http.Client{Timeout: 60 * time.Second}

type Action struct {
	Type  string `json:"type"`
	Label string `json:"label"`
	Hint  string `json:"hint"`
}

type WorkItem struct {
	ID         int     `json:"id"`
	Title      string  `json:"title"`
	Status     string  `json:"status"`
	Action     *Action `json:"action"`
	Permission *string `json:"permission"`
	Allowed    bool    `json:"allowed"`
	HasDraft   bool    `json:"has_draft"`
}

type WorkPage struct {
	Items      []WorkItem `json:"items"`
	NextCursor *string    `json:"next_cursor"`
	OpenTotal  int        `json:"open_total"`
}

// call performs one request and unwraps the envelope. The API always answers
// {"data": ...} or {"error": {"code","message"}}, so this returns the raw
// payload and leaves the caller to unmarshal it.
func call(method, path, key string, body any, hdr map[string]string) (json.RawMessage, error) {
	var reader io.Reader
	if body != nil {
		raw, err := json.Marshal(body)
		if err != nil {
			return nil, err
		}
		reader = bytes.NewReader(raw)
	}

	req, err := http.NewRequest(method, base+path, reader)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Authorization", "Bearer "+key)
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	for k, v := range hdr {
		req.Header.Set(k, v)
	}

	res, err := httpClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()

	raw, err := io.ReadAll(res.Body)
	if err != nil {
		return nil, err
	}

	// Every response is one of two envelopes.
	var envelope struct {
		Data  json.RawMessage `json:"data"`
		Error *struct {
			Code    string `json:"code"`
			Message string `json:"message"`
		} `json:"error"`
	}
	if err := json.Unmarshal(raw, &envelope); err != nil {
		return nil, fmt.Errorf("unreadable response (%d): %s", res.StatusCode, raw)
	}

	if envelope.Error != nil {
		return nil, fmt.Errorf("%s: %s", envelope.Error.Code, envelope.Error.Message)
	}
	return envelope.Data, nil
}

func main() {
	key := os.Getenv("OUTCOMEDEV_API_KEY")
	if key == "" {
		fmt.Fprintln(os.Stderr, "Set OUTCOMEDEV_API_KEY first.")
		os.Exit(1)
	}

	// 1. Authenticate.
	raw, err := call("GET", "/", key, nil, nil)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	var root struct {
		Desk string `json:"desk"`
		Key  struct {
			Name string `json:"name"`
		} `json:"key"`
	}
	if err := json.Unmarshal(raw, &root); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	fmt.Printf("Connected to the %s desk as %q.\n", root.Desk, root.Key.Name)

	// 2. List work.
	raw, err = call("GET", "/work?status=open&limit=50", key, nil, nil)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	var page WorkPage
	if err := json.Unmarshal(raw, &page); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	fmt.Printf("%d open, %d on this page.\n", page.OpenTotal, len(page.Items))

	// 3. Find something needing approval.
	var row *WorkItem
	for i := range page.Items {
		if page.Items[i].Action != nil && page.Items[i].Action.Type == "approve" {
			row = &page.Items[i]
			break
		}
	}
	if row == nil {
		fmt.Println("Nothing is waiting for approval.")
		return
	}
	fmt.Printf("#%d %s - %s\n", row.ID, row.Title, row.Action.Label)

	// Read it first: completing sends the prepared message.
	raw, err = call("GET", fmt.Sprintf("/work/%d", row.ID), key, nil, nil)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	var full struct {
		Messages []struct {
			Status  string  `json:"status"`
			Subject *string `json:"subject"`
			Body    *string `json:"body"`
		} `json:"messages"`
	}
	if err := json.Unmarshal(raw, &full); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	for _, m := range full.Messages {
		if m.Status == "drafted" {
			fmt.Println("--- about to go out ---")
			if m.Subject != nil {
				fmt.Printf("Subject: %s\n", *m.Subject)
			}
			if m.Body != nil {
				fmt.Println(*m.Body)
			}
			fmt.Println("-----------------------")
			break
		}
	}

	// 4. Complete it. The same key on a retry returns this response, not a second send.
	raw, err = call(
		"POST",
		fmt.Sprintf("/work/%d/complete", row.ID),
		key,
		map[string]any{"note": "Reviewed and approved.", "actor": "examples-go"},
		map[string]string{
			"Idempotency-Key": fmt.Sprintf("complete-%d-%s", row.ID, time.Now().UTC().Format("2006-01-02")),
		},
	)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	var done struct {
		ID           int    `json:"id"`
		Status       string `json:"status"`
		Failed       bool   `json:"failed"`
		Delivered    bool   `json:"delivered"`
		DeliveryNote string `json:"delivery_note"`
	}
	if err := json.Unmarshal(raw, &done); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	fmt.Printf("#%d is now %s.\n%s\n", done.ID, done.Status, done.DeliveryNote)

	// A 200 can still mean the message did not go.
	if done.Failed {
		os.Exit(1)
	}
}

What all four do the same way

DecisionWhy
Read the key from the environmentA key in a source file is a key in a repository
Treat data and error as the only two shapesEvery response in version 1 is one of them, with no exceptions
Branch on error.code, not on the statusTwo different 403s mean two very different things
Read the row before completing itCompleting sends the prepared message. It should be read first
Send an Idempotency-Key derived from the row and the actionA network retry must not be a second message to a counterparty
Check failed after a successful responseA 200 can carry news that nothing was sent
Accept waiting as a successful completionThe message went out and an answer is owed. That is what success looks like here
  • Quick start — the same walkthrough with commentary on each response.
  • Errors — what each code means, and which one is worth retrying.
  • MCP server — if a client does not need to be written at all.