Skip to main content
All writes go through a single endpoint. The action field determines the operation.
POST /api/v1/mutate

Add a Node

Creates a new node with an auto-generated ID.
curl -X POST "https://productbrain.com/api/v1/mutate" \
  -H "Authorization: Bearer pb_..." \
  -H "Content-Type: application/json" \
  -d '{
    "action": "add",
    "projectId": "my-project",
    "node": {
      "type": "job",
      "parentId": "approach-1",
      "data": {
        "label": "Upload progress bar reaches 100% for a 500MB file",
        "maturity": "mvp",
        "iteration": "MVP"
      }
    }
  }'
Response:
{
  "success": true,
  "node": {
    "id": "job-199",
    "type": "job",
    "parentId": "approach-1",
    "data": {
      "label": "Upload progress bar reaches 100% for a 500MB file",
      "maturity": "mvp",
      "iteration": "MVP"
    }
  }
}

Parent rules

TypeParent requiredParent type
GoalNoNone
NeedYesGoal
ApproachYesNeed
JobYesApproach
TaskNoNone

Update a Node

PATCH semantics. Only include fields you want to change. Existing fields are preserved.
curl -X POST "https://productbrain.com/api/v1/mutate" \
  -H "Authorization: Bearer pb_..." \
  -H "Content-Type: application/json" \
  -d '{
    "action": "update",
    "projectId": "my-project",
    "nodeId": "job-45",
    "data": { "status": "done" }
  }'
To mark a job as not done, set status to null.

Delete a Node

Hard delete. The node is removed from the table, but the full node is kept in the changelog, so a delete is recoverable with restore (below).
curl -X POST "https://productbrain.com/api/v1/mutate" \
  -H "Authorization: Bearer pb_..." \
  -H "Content-Type: application/json" \
  -d '{
    "action": "delete",
    "projectId": "my-project",
    "nodeId": "job-99"
  }'

Restore a Node

Recover a hard-deleted node from the changelog. It returns with its original id, type, parentId, and data. Identify it by nodeId (restores the most recent deletion of that node) or by a specific changelogId.
curl -X POST "https://productbrain.com/api/v1/mutate" \
  -H "Authorization: Bearer pb_..." \
  -H "Content-Type: application/json" \
  -d '{
    "action": "restore",
    "projectId": "my-project",
    "nodeId": "job-99"
  }'
  • 409 if a node with that id already exists (nothing to restore). 404 if no deletion is on record.
  • If the node’s original parent was also deleted, it comes back orphaned (dangling parentId). Restore the parent too, or reparent. The response _meta._tip flags this.
  • Restore does not cascade. It brings back the single node, not its former children.

Batch Mutations

Multiple operations in a single request. All-or-nothing. If any mutation fails validation, the entire batch is rejected.
curl -X POST "https://productbrain.com/api/v1/mutate" \
  -H "Authorization: Bearer pb_..." \
  -H "Content-Type: application/json" \
  -d '{
    "action": "batch",
    "projectId": "my-project",
    "mutations": [
      {
        "action": "add",
        "node": {
          "type": "job",
          "parentId": "approach-1",
          "data": { "label": "First checkpoint", "iteration": "MVP" }
        }
      },
      {
        "action": "update",
        "nodeId": "job-100",
        "data": { "status": "done" }
      }
    ]
  }'

Idempotency

Make any mutate call (single or batch) safe to retry by sending an Idempotency-Key header. An opaque per-operation string (a UUID is ideal):
curl -X POST "https://productbrain.com/api/v1/mutate" \
  -H "Authorization: Bearer pb_..." \
  -H "Idempotency-Key: 7b1f2c84-...-your-uuid" \
  -H "Content-Type: application/json" \
  -d '{ "action": "add", "projectId": "my-project", "node": { "type": "goal", "data": { "label": "..." } } }'
  • Replay. A second request with the same key and the same body returns the original response unchanged (with an Idempotent-Replayed: true header) instead of applying again. An agent that retries after a timeout never double-creates.
  • Conflict. The same key with a different body returns 409 (you reused a key for a new operation. Generate a fresh one).
  • In flight. If the first request is still running, a retry returns 409 (“in progress, retry shortly”).
  • Window. Keys are remembered for 24 hours, then expire. Scope is per user.
Without an Idempotency-Key, retries are not deduplicated, re-read with GET /api/v1/nodes or search and re-submit only what’s missing.

Validation

The API validates all mutations before applying them:
  • Label is required on all nodes
  • Parent must exist and be the correct type (need under goal, approach under need, etc.)
  • Fields must match node type, setting iteration on a goal or maturity on a task is rejected
  • Kano values: must-have, performance, delighter
  • Size values: skateboard, vespa, car, truck, antonov
  • Maturity values: mvp, releasable
  • Approach status values: development, validation, resolved, retired
Validation errors return { "success": false, "errors": ["..."] }.