Octopus
Octopus Developersgateway.octopusoperations.co.za/v1

Tutorial · about 15 minutes

Your first integration

A script that files a task against a real project. Node.js, no dependencies beyond what ships with it.

What you need

  • An API key with projects.read and tasks.manage.
  • Node 18 or newer, for built-in fetch.
bash
export OCTOPUS_API_KEY="oct_live_..."

Step 1: a client worth reusing

Because every response shares one envelope, one small wrapper removes all the repetition. Note that it raises on success: false rather than returning it: an integration that ignores a failure writes nothing and reports nothing, which is the worst of both.

octopus.js
const BASE = "https://gateway.octopusoperations.co.za/v1";

export async function octopus(path, { method = "GET", body } = {}) {
  const response = await fetch(BASE + path, {
    method,
    headers: {
      authorization: `Bearer ${process.env.OCTOPUS_API_KEY}`,
      ...(body ? { "content-type": "application/json" } : {}),
    },
    ...(body ? { body: JSON.stringify(body) } : {}),
  });

  const payload = await response.json();
  if (!payload.success) {
    // The API's own sentence is better than anything we would write here, and
    // requiredScope tells you precisely which permission to go and ask for.
    const detail = payload.requiredScope ? ` (needs ${payload.requiredScope})` : "";
    throw new Error(`${method} ${path} → ${response.status}: ${payload.error}${detail}`);
  }
  return payload;
}

Step 2: confirm the key

javascript
const me = await octopus("");
console.log("organisation:", me.organisation);
console.log("scopes:", me.scopes.map((s) => s.scope).join(", "));

If this throws a 401, the key is wrong or revoked. If it prints scopes you did not expect, remember that asking for .manage also grants the matching .read.

Step 3: find a project

A task must belong to one, so pick the first the key can see:

javascript
const { data: projects, count } = await octopus("/projects");
if (!count) throw new Error("No projects visible to this key.");

const project = projects[0];
console.log("filing against:", project.name, project.id);

Step 4: find someone to assign it to

Tasks are assigned to a person, by user id. If your key lacks personnel.read, take an id from an existing task instead: the code below tries the roster and falls back:

javascript
let assigneeId;
try {
  const { data: people } = await octopus("/personnel");
  assigneeId = people[0]?.userId ?? people[0]?.id;
} catch {
  const { data: tasks } = await octopus("/tasks");
  assigneeId = tasks.find((t) => t.assigneeId)?.assigneeId;
}
if (!assigneeId) throw new Error("Could not find anybody to assign to.");

Step 5: file the task

javascript
const { data: task } = await octopus("/tasks", {
  method: "POST",
  body: {
    title: "Inspect scaffolding on level 3",
    projectId: project.id,
    assigneeId,
    dueDate: "2026-09-30",
    priority: "high",
  },
});

console.log("created:", task.id);

Expect this to fail the first time

Leave out assigneeId and you get 400 assigneeId: Required. Leave out dueDate and you get the same about that. This is deliberate: a task nobody owns and nothing is due on cannot be chased, so Octopus refuses rather than creating one quietly.

Step 6: change it, and confirm

javascript
await octopus(`/tasks/${task.id}`, {
  method: "PATCH",
  body: { status: "in_progress" },
});

const { data: fresh } = await octopus(`/tasks/${task.id}`);
console.log("status is now:", fresh.status);

If the read looks stale, it is not broken

A write returns as soon as the change is recorded; the row you read is written a moment later. It is normally a few milliseconds, but do not assert on a read taken in the same tick as the write. If you need certainty, subscribe to the change rather than re-reading.

Where to go next