# Managing Resources (/docs/management)



Everything Ductor routes, runs, and dispatches is a **resource you manage
through the API**. These are the resources that shape the clearing surface: pools
are the markets <Term name="Work" /> enters, recipients are the
<Term name="Worker" />s a decision selects, and rules, workflows, publishers, and
connections define how work is priced, routed, and delivered. The dashboard is
optional — all of them are created, updated, inspected, and retired over the same
REST + Connect-RPC surface your application already calls. This section is the
operator's reference for that surface — what each resource is, *why* and *when*
you manage it, *how* to do it with real API calls, and *where* it lives (endpoint,
aggregate, storage).

<Callout title="What this section is not">
  This is the **management** (control-plane) surface — the CRUD and lifecycle
  operations that shape your routing and workflow topology. The **execution**
  surface (submitting an event to be routed, triggering a run) is covered under
  [Core Concepts](/docs/concepts/routing-pipeline). Managing *what* a decision
  chooses from lives here; making the decision lives there.
</Callout>

## The resource map [#the-resource-map]

Each resource has its own page. Read them in roughly this order — later
resources reference earlier ones.

<Cards>
  <Card title="Tenants" href="/docs/management/tenants">
    The top-level isolation boundary. Provision a tenant, set its quotas,
    metadata, and feature flags before anything else exists under it.
  </Card>

  <Card title="API Keys" href="/docs/management/api-keys">
    Mint, list, and revoke the tenant-scoped keys that authenticate every other
    call. Roles, scopes, expiry, and environment constraints.
  </Card>

  <Card title="Pools & Recipients" href="/docs/management/pools-and-recipients">
    Routing targets. A pool is a container with a strategy and kill-switch;
    recipients are the concrete endpoints with capacity and state.
  </Card>

  <Card title="Rules" href="/docs/management/rules">
    CEL-based routing rules attached to a pool — priority, enable/disable, and
    the cross-instance cache invalidation that makes edits take effect live.
  </Card>

  <Card title="Route Authoring" href="/docs/management/route-authoring">
    Route DSL v2 — the single public format for authoring routes as one
    versioned RouteAggregate per tenant that compiles down to the workflow DAG.
  </Card>

  <Card title="Workflows" href="/docs/management/workflows">
    Workflow *definitions* (draft → publish → immutable version) and *run*
    operations (pause, resume, cancel, signal, continue-as-new).
  </Card>

  <Card title="Experiments" href="/docs/management/experiments">
    Durable experiments for routing strategies, workflow steps, and agents,
    with sticky assignments, exposure receipts, and delayed scores.
  </Card>

  <Card title="Connections" href="/docs/management/connections">
    Tenant-scoped, encrypted credential bindings to third-party providers that
    connector steps dispatch through.
  </Card>

  <Card title="Configuration" href="/docs/management/configuration">
    The runtime-writable config surface — system config snapshots, feature
    flags, per-tenant overrides, and the routing pipeline mode.
  </Card>

  <Card title="Cases" href="/docs/management/cases">
    Create, assign, claim, resolve, and escalate human or agent-assisted operational work.
  </Card>

  <Card title="Environments" href="/docs/management/environments">
    Model dev, staging, and production boundaries with protection and readiness policies.
  </Card>

  <Card title="Secrets, Variables & Contexts" href="/docs/management/secrets-and-variables">
    Store encrypted secrets, plaintext variables, and tenant-scoped context data.
  </Card>

  <Card title="Publishers" href="/docs/management/publishers">
    Govern inbound traffic sources with caps, quality thresholds, pricing, and delivery statistics.
  </Card>
</Cards>

## Two API generations: `/api` and `/api/v2` [#two-api-generations-api-and-apiv2]

Ductor's REST surface is generated from the same protobuf definitions as the
Connect-RPC API, so every operation exists in both dialects. As the platform
grew, newer services were introduced under a `/api/v2` prefix while the
established resource CRUD kept its `/api` paths. Both are current and supported —
the prefix simply tells you which generation a service belongs to.

| Generation | Used by                                                                                                     | Path style                                                                     |
| ---------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `/api`     | Tenants, pools, recipients, rules, experiments, workflow *definitions*, workflow *run reads*, system config | Plural resource nouns: `POST /api/pools`, `GET /api/tenants/{tenant_id}`       |
| `/api/v2`  | API keys, workflow *run control* + signals, connections + connect sessions, runtime config                  | Sub-resources and RPC-style verbs: `POST /api/v2/workflow-runs/{run_id}:pause` |

<Callout type="info">
  The `:verb` suffix you'll see on some `/api/v2` paths (`:trigger`, `:pause`,
  `:approve`) is the standard grpc-gateway custom-method syntax. It's a literal
  part of the URL — `POST /api/v2/workflow-runs/{run_id}:cancel` is one path,
  not a path plus a query.
</Callout>

## Authentication and tenancy on every call [#authentication-and-tenancy-on-every-call]

Every management endpoint is authenticated and tenant-scoped. Two things travel
with each request:

* **`Authorization: Bearer <token>`** — a JWT from your OIDC provider, or a
  Ductor [API key](/docs/management/api-keys) as the fallback credential.
* **`X-Tenant-ID: <uuid>`** — the tenant the call operates within.

```bash
export DUCTOR_API_KEY="duk_live_..."
export DUCTOR_TENANT="9c8b7a6d-1234-4e5f-8a9b-0c1d2e3f4a5b"

curl -s https://api.ductor.io/api/pools \
  -H "Authorization: Bearer $DUCTOR_API_KEY" \
  -H "X-Tenant-ID: $DUCTOR_TENANT"
```

<Callout title="The tenant comes from the request context, never the body">
  Where a resource is tenant-scoped, the server takes the tenant identity from
  the authenticated request — the `X-Tenant-ID` header and token claims — and
  **ignores any `tenant_id` in the payload**. You cannot create a resource in a
  tenant you are not authenticated against. This is enforced uniformly; see
  [Tenancy](/docs/concepts/tenancy) for why isolation is an end-to-end
  invariant. Full details of the auth model — JWT verification, API-key
  fallback, RBAC — live in the [Authentication](/docs/auth) section.
</Callout>

### Authorization: resource + action + roles [#authorization-resource--action--roles]

Each RPC declares its own authorization requirement. The interceptor chain
evaluates three things against your token:

* **`resource_type`** — what kind of object the call touches (`pool`,
  `recipient`, `rule`, `workflow`, `workflow_run`, `workflow_definition`,
  `connector`, `tenant`, `config`, …).
* **`action`** — `read`, `write`, `delete`, or `admin`.
* **`roles`** — an any-of role gate on sensitive operations. Ductor's built-in
  roles are `viewer`, `operator`, `admin`, and `service`. Tenant provisioning
  and API-key minting, for example, require `admin`.

Each resource page lists the exact `resource_type:action` (and any role gate)
for its operations so you can scope keys to least privilege.

## Conventions shared across every resource [#conventions-shared-across-every-resource]

### Pagination [#pagination]

List endpoints take a `pagination` object and return a cursor. Page size caps at
**1000**, defaults to **50**.

```bash
# First page
curl -s "https://api.ductor.io/api/pools?pagination.page_size=25" \
  -H "Authorization: Bearer $DUCTOR_API_KEY" -H "X-Tenant-ID: $DUCTOR_TENANT"

# Next page — feed back the token you were handed
curl -s "https://api.ductor.io/api/pools?pagination.page_size=25&pagination.page_token=eyJvZmZzZXQiOjI1fQ==" \
  -H "Authorization: Bearer $DUCTOR_API_KEY" -H "X-Tenant-ID: $DUCTOR_TENANT"
```

Every list response carries a `pagination` block:

```json
{
  "pagination": {
    "next_page_token": "eyJvZmZzZXQiOjUwfQ==",
    "total_count": 250
  }
}
```

`next_page_token` is an **opaque cursor** — treat it as a blob, don't parse it.
It's empty on the last page. `total_count` may be `-1` when the total is too
expensive to compute.

### Response metadata [#response-metadata]

Every response — success or list — includes a `metadata` object for
observability:

```json
{
  "metadata": {
    "trace_id": "trace-abc123def456",
    "request_id": "req-789ghi012",
    "fetched_at": "2026-07-11T10:30:00Z"
  }
}
```

`trace_id` is the OpenTelemetry trace ID — quote it in a support ticket and the
whole request is reconstructable end to end.

### Errors are Problem JSON [#errors-are-problem-json]

REST errors follow [RFC 9457 Problem
Details](https://www.rfc-editor.org/rfc/rfc9457). Domain sentinels map to stable
HTTP statuses, and field-level validation failures are itemized:

```json
{
  "type": "https://ductor.io/errors/validation",
  "title": "Invalid argument",
  "status": 400,
  "detail": "name: must not be empty",
  "errors": [
    { "field": "name", "description": "must not be empty", "reason": "FIELD_VIOLATION" }
  ]
}
```

| HTTP  | Meaning             | Typical cause                                                  |
| ----- | ------------------- | -------------------------------------------------------------- |
| `400` | Invalid argument    | Malformed body, failed field validation                        |
| `401` | Unauthenticated     | Missing/expired token                                          |
| `403` | Permission denied   | Token lacks the required role or scope                         |
| `404` | Not found           | Resource ID doesn't exist in this tenant                       |
| `409` | Conflict            | Uniqueness violation, or a state transition that isn't allowed |
| `412` | Failed precondition | Optimistic-lock version mismatch (see below)                   |
| `429` | Quota exceeded      | A tenant quota or rate limit was hit                           |

The Connect-RPC surface returns the equivalent Connect/gRPC codes for the same
sentinels.

### Optimistic concurrency [#optimistic-concurrency]

Mutable resources that a coordinator or multiple operators can touch carry a
**version counter** (`record_version` on pools, `draft_revision` on workflow
drafts, `db_record_version` on workflow runs). Where an operation accepts an
`expected_version`, the server rejects the write with `412 Failed Precondition`
if the stored version has moved on — you re-read, reconcile, and retry. This is
the same optimistic-locking discipline the coordinator uses internally; see
[Optimistic Locking](/docs/concepts/optimistic-locking).

## Where to start [#where-to-start]

If you're standing up a tenant from scratch, the path is linear:

<Cards>
  <Card title="1. Provision a tenant" href="/docs/management/tenants">
    Create the isolation boundary and set its quotas.
  </Card>

  <Card title="2. Mint an API key" href="/docs/management/api-keys">
    Get a credential scoped to the roles your integration needs.
  </Card>

  <Card title="3. Build routing topology" href="/docs/management/pools-and-recipients">
    Create pools and recipients, then attach rules.
  </Card>

  <Card title="4. Publish a workflow" href="/docs/management/workflows">
    Draft, validate, and publish a definition; then trigger and operate runs.
  </Card>
</Cards>
