> ## Documentation Index
> Fetch the complete documentation index at: https://docs.useorgx.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Handoff

> The handoff resource: transfer responsibility between stages with a six-state machine, idempotent writes, and expected-version control.

A handoff transfers responsibility between process stages. It is an
event-sourced aggregate with an explicit state machine: every transition is a
command carrying an `Idempotency-Key` and the `expected_aggregate_version`
you last observed, so two actors cannot both claim the same handoff without
one of them getting a `409`.

## Wire shape

| Field | Type | Meaning |
| - | - | - |
| `schemaVersion` | string | Contract version. |
| `id` | UUID | Handoff ID (derived from your idempotency key if omitted). |
| `workspaceId` | UUID | Tenant scope. |
| `handoffKey` | lowercase identifier | Stable key, for example `intake_to_delivery`. |
| `fromStageKey`, `toStageKey` | lowercase identifiers | Source and destination stages. |
| `sourceProcessRef` | UUID, nullable | The operating process this handoff belongs to. |
| `sourceRevisionRef` | UUID, nullable | The spec revision that defines the stage pair. |
| `title` | string ≤240 | Human-readable transfer description. |
| `summary` | string ≤4000, nullable | Context. |
| `priority` | `low` `normal` `high` `urgent` | Defaults to `normal`. |
| `slaMinutes` | integer > 0, nullable | Service-level target. |
| `dueAt` | ISO datetime, nullable | Deadline. |
| `currentActor` | actor ref, nullable | Set while `claimed` or `escalated`. |
| `proofRequirements` | JSON object array (≤20) | What fulfillment must include. |
| `result` | JSON object, nullable | The fulfillment payload. |
| `status` | enum | See the state machine. |
| `createdAt`, `updatedAt` | ISO datetime | Timestamps. |

Reads also include `aggregateVersion` — the number you echo back as
`expected_aggregate_version` on the next transition.

## State machine

States: `proposed`, `claimed`, `returned`, `fulfilled`, `escalated`,
`cancelled`. Terminal states: `fulfilled` and `cancelled`.

| Transition | Allowed from | Endpoint |
| - | - | - |
| `claim` | `proposed`, `returned` | `POST /api/v1/handoffs/{handoffId}/claim` |
| `fulfill` | `claimed` | `POST /api/v1/handoffs/{handoffId}/fulfill` |
| `return` | `claimed`, `escalated` | `POST /api/v1/handoffs/{handoffId}/return` |
| `escalate` | `proposed`, `claimed`, `returned` | `POST /api/v1/handoffs/{handoffId}/escalate` |
| `cancel` | `proposed`, `claimed`, `returned`, `escalated` | `POST /api/v1/handoffs/{handoffId}/cancel` |

An illegal transition returns `409 handoff_conflict`; so does a stale
`expected_aggregate_version`.

## Operations

All routes accept `Authorization: Bearer oxk_...` or a web-app session;
every mutation requires an `Idempotency-Key` header.

| Operation | Method + path | Body (all fields snake\_case) |
| - | - | - |
| Create | `POST /api/v1/handoffs` | `workspace_id`*, `handoff_key`*, `from_stage_key`*, `to_stage_key`*, `title`\*, `summary`, `priority`, `sla_minutes`, `due_at`, `proof_requirements`, `source_process_ref`, `source_revision_ref`, `handoff_id` |
| List | `GET /api/v1/handoffs` | `workspace_id`, `limit` (default 50, max 200; `meta.truncated` signals a capped replay) |
| Get | `GET /api/v1/handoffs/{handoffId}` | `workspace_id` |
| Claim | `POST .../claim` | `workspace_id`*, `expected_aggregate_version`* |
| Fulfill | `POST .../fulfill` | `workspace_id`*, `expected_aggregate_version`*, `result`\* (JSON object) |
| Return | `POST .../return` | `workspace_id`*, `expected_aggregate_version`* |
| Escalate | `POST .../escalate` | `workspace_id`*, `expected_aggregate_version`* |
| Cancel | `POST .../cancel` | `workspace_id`*, `expected_aggregate_version`* |
| Update / delete | Not exposed | The handoff record is its event history; `cancel` is the terminal negative. |

```bash theme={"dark"}
# Propose a handoff
curl https://useorgx.com/api/v1/handoffs \
  -X POST \
  -H "Authorization: Bearer $ORGX_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: intake-42-to-delivery' \
  -d '{
    "workspace_id": "3d7e4f5a-6b7c-8d9e-0f1a-2b3c4d5e6f7a",
    "handoff_key": "intake_to_delivery",
    "from_stage_key": "intake",
    "to_stage_key": "delivery",
    "title": "Hand intake #42 to delivery"
  }'

# Claim it (aggregateVersion 1 came back from the create)
curl https://useorgx.com/api/v1/handoffs/4a3b2c1d-.../claim \
  -X POST \
  -H "Authorization: Bearer $ORGX_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: intake-42-claim-me' \
  -d '{"workspace_id":"3d7e4f5a-...","expected_aggregate_version":1}'
```

Mutations return `201` (or `200` on idempotent replay) with
`{ "data": <handoff>, "meta": { "duplicate", "aggregateVersion", ... } }`.

### Transition bodies are exactly two fields

`return`, `escalate`, and `cancel` accept **only** `workspace_id` and
`expected_aggregate_version`, both required. The schema is strict: there is no
`reason`, `note`, `comment`, or `actor` field, and sending one is a `400`
rather than being ignored. `claim` is the same two fields; `fulfill` adds a
required `result` (JSON object).

The transition `400` responses carry `{ "error": { "code", "message" } }` with
no `details` — the code is what tells you which call failed:

| Endpoint | `400` code | `500` code |
| - | - | - |
| `/return` | `invalid_handoff_return` | `handoff_return_failed` |
| `/escalate` | `invalid_handoff_escalation` | `handoff_escalation_failed` |
| `/cancel` | `invalid_handoff_cancellation` | `handoff_cancellation_failed` |

All five transitions share `404 handoff_not_found` (unknown ID, or a
`workspace_id` that is not the authed workspace), `409 handoff_conflict`
(illegal transition or stale version), `503 handoff_unavailable`, and
`400 missing_idempotency_key` / `400 invalid_handoff_id`.

```bash theme={"dark"}
# Return a claimed handoff to the queue
curl https://useorgx.com/api/v1/handoffs/4a3b2c1d-5e6f-7a8b-9c0d-1e2f3a4b5c6d/return \
  -X POST \
  -H "Authorization: Bearer $ORGX_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: intake-42-return-001' \
  -d '{
    "workspace_id": "3d7e4f5a-6b7c-8d9e-0f1a-2b3c4d5e6f7a",
    "expected_aggregate_version": 2
  }'
```

## Where the IDs come from

| Field | Obtain it from |
| - | - |
| `handoffId` | `POST /api/v1/handoffs` response `data.id`, or `GET /api/v1/handoffs` list rows |
| `expected_aggregate_version` | The previous response's `meta.aggregateVersion`, or `GET /api/v1/handoffs/{handoffId}` `data.aggregateVersion` |
| `source_process_ref` | An [operating process](/docs/api/entities/operating-process) `id` |
| `source_revision_ref` | That process's `currentRevisionRef` or a revision `id` from `GET /operating-processes/{processId}` |
| `handoff_key`, stage keys | You define them; stage keys should match the process spec revision's stages |
| `workspace_id` | `GET /api/v1/me` → `data.default_workspace_id` or `data.workspaces[].id` |

## MCP equivalents

No MCP tool operates on this resource today — stage handoffs are REST-first.
(The MCP `orgx_spawn` tool's `handoff` action delegates agent tasks, which is
a different mechanism from stage handoffs.)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.