# Activate a drip workflow

`POST /v1/workflows/{id}/activate`

- Authentication: required (Bearer token)
- Required scope: `api.workflows.manage`

Arms a `draft` or `paused` workflow. Any other status is
`409 conflict`, and the stored source must validate clean — an
`invalid` definition cannot enrol or step. On writes `{id}` resolves as
a UUID or, failing that, as the workflow's org-unique key.

`mode` is `live` (the default when the body is absent) or `shadow`.
Any other value is a `422` rather than being ignored. Classified
destructive and refused for a `sk_test_` credential, because
**enrolment cannot be undone**: a contact enrolled into a journey is in
it, and pausing afterwards stops the journey stepping rather than
un-enrolling anybody.

**`mode: "shadow"` is the staging state on real data.** The workflow
goes `active` and enrols real contacts through the real triggers on the
real clock, but every externally visible effect — a send, a `set`, an
`add to list` — is RECORDED on the run timeline instead of performed.
Nothing leaves. `cohort: {list: "<key>"}` bounds who may enrol into the
rehearsal, and is a `422` with `mode: "live"`: the two mean opposite
things about who is about to be enrolled. The send-approval gate is
neither read nor written for a shadow, because a rehearsal produces no
batch for anybody to approve. Read what it recorded with
`GET /v1/workflows/{id}/enrollment-decisions` and the funnel, then
`POST /v1/workflows/{id}/go-live` arms the same definition for real
with no re-authoring.

Two things happen besides the status change, and the response reports
both:

- **The send-approval gate.** Forced ON when the stored source contains
  a step that sends email or calls a webhook at any depth — a `send` or
  a `call webhook`, inside an `if`, a `split`, a `repeat`, a
  `wait … timeout` or a `failed:` arm — so every send and every call
  waits for a person. LEFT EXACTLY AS IT WAS when the source contains
  none: a flow whose only steps are `add to list` or `set attr.x`
  reaches nothing outside SendOps and so produces nothing for anybody to
  approve, and forcing the gate would park it behind a queue nothing
  will ever drain. `require_send_approval` and `send_steps` in the
  response say which half applied.
- **The `enroll existing` backfills.** `backfills[]` lists one entry per
  trigger that will scan history, with the window it covers and an
  approximate count of what that window holds. An empty array means the
  workflow enrols forward only.

## Path parameters

- `id` (string, required) — The workflow's UUID or, when the value does not parse as a UUID, its org-unique key. Unknown handles read as `404 not_found`. Accepted on every workflow route, read and write alike.

## Request body

Content type: `application/json`

```json
{
  "mode": "live",
  "cohort": {
    "list": "string"
  }
}
```

## Example request

```bash
curl -X POST 'https://api.sendops.dev/v1/workflows/string/activate' \
  -H "Authorization: Bearer $SENDOPS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"mode":"live","cohort":{"list":"string"}}'
```

## Responses

### 200 — The activated workflow, the gate in effect, and what activation started

Content type: `application/json`

```json
{
  "workflow": {
    "id": "00000000-0000-0000-0000-000000000000",
    "name": "string",
    "key": "string",
    "description": "string",
    "status": "string",
    "invalid_reason": "string",
    "require_send_approval": true,
    "send_mode": "live",
    "shadow_started_at": "2026-05-17T20:00:00Z",
    "shadow_cohort_list_id": "00000000-0000-0000-0000-000000000000",
    "shadow_cohort": {
      "id": "00000000-0000-0000-0000-000000000000",
      "key": "string",
      "name": "string",
      "member_count": 0
    },
    "current_version": 0,
    "run_counts": {},
    "created_at": "2026-05-17T20:00:00Z",
    "updated_at": "2026-05-17T20:00:00Z",
    "source": "string",
    "content_hash": "string",
    "warnings": [
      "string"
    ],
    "diagnostics": [
      {
        "line": 0,
        "column": 0,
        "severity": "string",
        "message": "string"
      }
    ],
    "requires": [
      {
        "kind": "template",
        "key": "string",
        "status": "ok",
        "detail": "string",
        "fix": "string"
      }
    ]
  },
  "require_send_approval": true,
  "send_steps": 0,
  "backfills": [
    {
      "trigger_kind": "string",
      "key": "string",
      "status": "string",
      "window_start": "2026-05-17T20:00:00Z",
      "window_end": "2026-05-17T20:00:00Z",
      "estimate": 0
    }
  ],
  "mode": "live",
  "cohort": {
    "id": "00000000-0000-0000-0000-000000000000",
    "key": "string",
    "name": "string",
    "member_count": 0,
    "matching_total": 0
  }
}
```

### 401 — Missing, malformed, or unknown API key

Content type: `application/problem+json`

```json
{
  "type": "https://example.com",
  "title": "string",
  "status": 0,
  "detail": "string",
  "code": "invalid_key",
  "request_id": "string",
  "retry_after": 0,
  "retention_days": 0,
  "scope": "string",
  "resource": "string",
  "errors": [
    {
      "field": "string",
      "reason": "string"
    }
  ],
  "attribute_id": "00000000-0000-0000-0000-000000000000",
  "content_hash": "string",
  "differs": [
    "string"
  ]
}
```

### 403 — Either the credential lacks the required scope (`code: invalid_scope`), or it is bound to the `test` environment and this operation is irreversible (`code: test_environment_forbidden`). Branch on `code`: the first is fixed by granting the scope, the second only by using a live credential. See the "Live and test credentials" section of the API description.

Content type: `application/problem+json`

```json
{
  "type": "https://example.com",
  "title": "string",
  "status": 0,
  "detail": "string",
  "code": "invalid_key",
  "request_id": "string",
  "retry_after": 0,
  "retention_days": 0,
  "scope": "string",
  "resource": "string",
  "errors": [
    {
      "field": "string",
      "reason": "string"
    }
  ],
  "attribute_id": "00000000-0000-0000-0000-000000000000",
  "content_hash": "string",
  "differs": [
    "string"
  ]
}
```

### 404 — Resource not found

Content type: `application/problem+json`

```json
{
  "type": "https://example.com",
  "title": "string",
  "status": 0,
  "detail": "string",
  "code": "invalid_key",
  "request_id": "string",
  "retry_after": 0,
  "retention_days": 0,
  "scope": "string",
  "resource": "string",
  "errors": [
    {
      "field": "string",
      "reason": "string"
    }
  ],
  "attribute_id": "00000000-0000-0000-0000-000000000000",
  "content_hash": "string",
  "differs": [
    "string"
  ]
}
```

### 409 — The mutation is rejected by a state rule rather than a bad request. The
`code` is `conflict`.

Content type: `application/problem+json`

```json
{
  "type": "https://example.com",
  "title": "string",
  "status": 0,
  "detail": "string",
  "code": "invalid_key",
  "request_id": "string",
  "retry_after": 0,
  "retention_days": 0,
  "scope": "string",
  "resource": "string",
  "errors": [
    {
      "field": "string",
      "reason": "string"
    }
  ],
  "attribute_id": "00000000-0000-0000-0000-000000000000",
  "content_hash": "string",
  "differs": [
    "string"
  ]
}
```

### 422 — A query parameter, path value or body field failed validation.

The body is a `validation_failed` Problem. When the refusal is about a
reference — a segment key, template slug, topic or attribute name the
organization does not have — it additionally carries `validation_code`,
`field`, `line`/`column`, `missing`, `candidates` and `next_step`, so a
client can correct the call without a second round of guessing. See
`ValidationProblem`.

Content type: `application/problem+json`

```json
{
  "type": "https://example.com",
  "title": "string",
  "status": 0,
  "detail": "string",
  "code": "invalid_key",
  "request_id": "string",
  "retry_after": 0,
  "retention_days": 0,
  "scope": "string",
  "resource": "string",
  "errors": [
    {
      "field": "string",
      "reason": "string"
    }
  ],
  "attribute_id": "00000000-0000-0000-0000-000000000000",
  "content_hash": "string",
  "differs": [
    "string"
  ],
  "validation_code": "invalid_syntax",
  "field": "string",
  "line": 0,
  "column": 0,
  "missing": [
    {
      "kind": "segment",
      "key": "string"
    }
  ],
  "candidates": {},
  "next_step": "string"
}
```

### 429 — Per-org rate limit exceeded

Content type: `application/problem+json`

```json
{
  "type": "https://example.com",
  "title": "string",
  "status": 0,
  "detail": "string",
  "code": "invalid_key",
  "request_id": "string",
  "retry_after": 0,
  "retention_days": 0,
  "scope": "string",
  "resource": "string",
  "errors": [
    {
      "field": "string",
      "reason": "string"
    }
  ],
  "attribute_id": "00000000-0000-0000-0000-000000000000",
  "content_hash": "string",
  "differs": [
    "string"
  ]
}
```

### 500 — Unexpected server-side failure. The `code` is `internal_error`. The
`request_id` field can be quoted to SendOps support to investigate.

Content type: `application/problem+json`

```json
{
  "type": "https://example.com",
  "title": "string",
  "status": 0,
  "detail": "string",
  "code": "invalid_key",
  "request_id": "string",
  "retry_after": 0,
  "retention_days": 0,
  "scope": "string",
  "resource": "string",
  "errors": [
    {
      "field": "string",
      "reason": "string"
    }
  ],
  "attribute_id": "00000000-0000-0000-0000-000000000000",
  "content_hash": "string",
  "differs": [
    "string"
  ]
}
```
