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

# Launch a campaign — activate + run the launch pipeline (alias /send)

> Marks the campaign ready: sets `status: active` and runs the full
launch pipeline campaign-keyed (sender gate, duplicate auto-cancel,
per-account scheduling). Idempotent on an already-active campaign.

**Launch is "mark ready", never "force".** There are NO override
knobs — send windows, daily caps, spacing, and duplicate settings
are the campaign's own persisted config, set through the agent,
never per-call. `POST /campaigns/{campaignId}/send` is an alias.
`?dryRun=true` returns `{ dryRun: true, campaignId, wouldLaunch: true }`
with no writes.




## OpenAPI

````yaml /openapi-v2.yaml post /campaigns/{campaignId}/launch
openapi: 3.1.0
info:
  title: Origami Agent API
  version: '2.0'
  description: >
    The Origami **v2 API** is the single canonical public surface,

    organized into five segments — **Projects** (tenancy), **Agents**,

    **Workspace**, **Outreach** (campaigns, sequences, steps), and

    **Account**. Drive AI workers from a prompt, read and write table

    data (rows, cells, upsert, enrichment runs), manage documents and

    workspaces, control the sequencer and campaigns, run recurring

    scheduled agents, and read account/credit state.


    **Objects are self-describing** (Stripe-style): every object in a

    response carries an `object` field naming its type (`"agent"`,

    `"run"`, `"table"`, `"campaign"`, ...), and every list endpoint

    returns the one list envelope

    `{ "object": "list", "items": [...], "nextCursor": string|null, "url":
    string }`.


    **v1 is deprecated** (still fully functional — no removal date in

    this release). New integrations should target v2 only. See the

    [v1 → v2 migration guide](./api-v1-to-v2-migration) for the route

    map; in short:


    | v1 | v2 |

    | --- | --- |

    | `GET /tables` | `GET /api/v2/tables` |

    | `GET /tables/:id/rows` | `GET /api/v2/tables/:tableId/rows` |

    | `POST /tables/:id/rows` (insert) | `POST
    /api/v2/tables/:tableId/rows/upsert` (upsert; no bare insert) |

    | `GET /batches/:id` | `GET /api/v2/enrichment-runs/:runId` (`GET
    /api/v2/batches/:batchId` remains a deprecated alias) |

    | `GET /credits` | `GET /api/v2/account/credits` |


    Errors use the canonical envelope `{ error, code, details?, handoff? }`;

    any 4xx the user can fix in-app carries a forwardable `handoff` link.


    ## Tenancy — parent-wide keys & the `x-origami-project` header


    Every API key is **parent-wide**: bound to the parent (agency)

    organization and able to act on the parent and any of its

    **projects** (child orgs). Send the

    **`x-origami-project: <projectId>`** header on any v2 request to

    scope it to that project — all org-scoped resources (workspaces,

    tables, agents, outreach) then operate inside the project. Omit the

    header to act on the parent. Two exceptions: `/projects/*` manages

    the projects themselves from the parent (the header is ignored

    there, never rejected), and `/account` is always parent-scoped.


    The header fails closed: a malformed id →

    `400 VALIDATION_ERROR`; an unknown, cross-parent, or deleted

    project → `404 PROJECT_NOT_FOUND`. The plan gate

    (`canUseApi`) and request-rate limits stay keyed to the parent, and

    the concurrent-agent pool is shared across the whole parent.

    Credits spent inside a project draw on the parent's shared wallet,

    subject to the project's optional `monthlyCredits` budget cap.


    ## Transport — async by default


    `POST /agents` and `POST /agents/:id/runs` respond `202 Accepted`

    with the initial run object (`status: "running"`) as soon as the

    run is admitted. The actual agent work runs in the background.

    Callers poll `GET /agents/:id/runs/:runId` until `status !==

    "running"` to read the terminal state.


    Every endpoint returns plain `application/json`. There is no

    NDJSON, no heartbeats, no `tail -1`, no pre-stream vs in-stream

    error branches. Closing the HTTP socket has no effect on the run

    — there is no `abortOnDisconnect` knob.


    SDKs hide the polling — `await origami.agents.create({ prompt })`

    returns the final terminal `Run` object. Direct REST consumers

    poll `GET /agents/:id/runs/:runId` and honor the `Retry-After`

    header it returns while the run is still `running` (currently

    `15` seconds). The header is omitted on terminal responses —

    that absence is the signal to stop polling. Runs typically take

    1–5 minutes; polling faster than the hint does not make a run

    finish sooner.


    ## Pagination — cursor only


    Every list endpoint takes the same parameter pair — `limit`

    (default 50, max 100; row queries allow up to 200) and `cursor`

    (opaque; pass back the previous page's `nextCursor`) — and returns

    the one envelope `{ object: "list", items, nextCursor, url }`.

    `nextCursor: null` marks the last page. There is no

    `page`/`pageSize` and no `offset`. Per-endpoint filters (e.g.

    `?search=`, `?status=`, `?enabled=`) compose with `cursor`/`limit`.


    ## Idempotency


    Any `POST` may send an **`Idempotency-Key`** header (1–255

    printable ASCII chars). The first response for a key is recorded

    and replayed verbatim for retries with the same key + request for

    24 hours; replayed responses carry the `Idempotency-Replay: true`

    header. Reusing a key with a *different* method/path/body — or

    racing a still-in-flight original — is `409 IDEMPOTENCY_ERROR`.

    Row upserts additionally dedup on the body `batchId`

    (domain-level idempotency, independent of transport retries).


    ## Rate limits


    Same layers as v1: **per IP** (300 req/min) and **per organization**

    (100 req/min). The real scarce resource for agent runs is the

    per-org concurrent-agent slot — see `CONCURRENT_LIMIT_EXCEEDED`.


    | Scope | Limit |

    |-------|-------|

    | Per client IP | **300 requests / minute** |

    | Per organization | **100 requests / minute** |

    | Concurrent runs (per organization) | **plan-tunable** (1 on starter, 3 on
    pro, 10 on scale, 20 on ultra, unlimited on enterprise) |


    Rate-limit headers (`X-RateLimit-Limit-{IP|Global}`,

    `X-RateLimit-Remaining-*`, `X-RateLimit-Reset-*`) and `Retry-After`

    are inherited from v1.


    ## Plan-tier policy


    Agent-loop knobs (`maxSteps`, `timeoutSeconds`) are **not** request

    parameters — they're internal config gated by the plan tier. The

    chat agent's intrinsic 20-minute `AGENT_TIMEOUT_MS` is the wall-

    clock ceiling.


    | Plan | Default model | Available models | internal maxSteps |

    | --- | --- | --- | --- |

    | `starter` | `origami-lite` | `origami-lite` | 30 |

    | `pro` | `origami-max` | `origami-lite`, `origami-max` | 50 |

    | `scale` | `origami-max` | `origami-lite`, `origami-max` | 75 |

    | `ultra` | `origami-max` | `origami-lite`, `origami-max` | 100 |

    | `enterprise` | `origami-max` | `origami-lite`, `origami-max` | 200 |


    ## Errors


    All error codes are `UPPERCASE_SNAKE_CASE` and live on `code`. The

    response body always has `error` (human-readable) and `code`;

    error-specific fields (e.g. `creditsRequired`, `topUpUrl`,

    `currentRun`, `activeSessions`) live alongside at the top level

    when they're stable parts of the contract.


    Non-`completed` terminal *run* states (`timed_out`, `errored`,

    `cancelled`, `needs_input`, `step_cap_hit`) are surfaced on the

    `Run` object's single `status` field — not as HTTP error codes.

    Callers branch on the run object.


    Notable codes beyond the per-endpoint ones: `IDEMPOTENCY_ERROR`

    (409 — `Idempotency-Key` reused with a different request, or the

    original is still in flight), `TOO_MANY_ROWS` (413 — a CSV upsert

    exceeds the 100-row per-request cap), `CAMPAIGN_NOT_FOUND` (404 —

    missing or cross-org campaign id), `SEQUENCE_NOT_FOUND` (404 —

    missing or cross-org sequence id), and `PROJECT_NOT_FOUND` (404 —

    the `x-origami-project` header names an unknown, cross-parent, or

    deleted project).


    ## Webhooks


    Sequencer events fan out via HTTPS POSTs you configure in

    **Settings → Developers → Webhooks**. See the

    [webhooks reference](/webhooks/overview) and the machine-readable

    [openapi-webhooks.yaml](/openapi-webhooks.yaml) spec for working

    receiver scaffolding.
servers:
  - url: https://origami.chat/api/v2
    description: Production
security:
  - bearerAuth: []
tags:
  - name: Projects
    description: >-
      Projects (child orgs) under the parent org — list, get, create, update,
      delete. Managed from the parent; the `x-origami-project` header is ignored
      on these paths.
  - name: Agents
    description: Create, list, and archive AI workers (a.k.a. agents).
  - name: Runs
    description: Send prompts to an agent, poll status, and cancel.
  - name: Tables
    description: Read table state and lifetime credit cost.
  - name: Workspace
    description: Workspaces, tables, columns, rows, cells, enrichment runs, and documents.
  - name: Campaigns
    description: >-
      First-class outreach campaigns — list, get, read people + stats, create +
      edit (agentic), launch/pause/resume, delete.
  - name: Sequences
    description: >-
      Read per-recipient sequences (with steps inline), stop and delete them.
      Content edits go through the agentic campaign edit.
  - name: Scheduled agents
    description: Recurring agents (cron) — CRUD, enable/disable, trigger, run history.
  - name: Account
    description: Org account state — plan, capabilities, credit balance.
paths:
  /campaigns/{campaignId}/launch:
    parameters:
      - $ref: '#/components/parameters/CampaignId'
    post:
      tags:
        - Campaigns
      summary: Launch a campaign — activate + run the launch pipeline (alias /send)
      description: |
        Marks the campaign ready: sets `status: active` and runs the full
        launch pipeline campaign-keyed (sender gate, duplicate auto-cancel,
        per-account scheduling). Idempotent on an already-active campaign.

        **Launch is "mark ready", never "force".** There are NO override
        knobs — send windows, daily caps, spacing, and duplicate settings
        are the campaign's own persisted config, set through the agent,
        never per-call. `POST /campaigns/{campaignId}/send` is an alias.
        `?dryRun=true` returns `{ dryRun: true, campaignId, wouldLaunch: true }`
        with no writes.
      operationId: launchCampaign
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - in: query
          name: dryRun
          schema:
            type: boolean
            default: false
      responses:
        '200':
          description: The transition result — or the dryRun report.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/CampaignTransition'
                  - type: object
                    required:
                      - dryRun
                      - campaignId
                      - wouldLaunch
                    properties:
                      dryRun:
                        type: boolean
                        const: true
                      campaignId:
                        type: string
                        format: uuid
                      wouldLaunch:
                        type: boolean
                        const: true
        '404':
          description: CAMPAIGN_NOT_FOUND.
components:
  parameters:
    CampaignId:
      in: path
      name: campaignId
      required: true
      description: The campaign id (a first-class `campaigns` row).
      schema:
        type: string
        format: uuid
    IdempotencyKey:
      in: header
      name: Idempotency-Key
      required: false
      description: |
        Transport-level replay protection — any v2 `POST` may send it
        (1–255 printable ASCII characters; UUIDs recommended). The first
        response for a key is recorded and replayed verbatim for retries
        with the same key + method/path/body for 24 hours; replayed
        responses carry the `Idempotency-Replay: true` response header.
        Reusing a key with a *different* request — or retrying while the
        original is still in flight — returns `409 IDEMPOTENCY_ERROR`.
        5xx responses are never recorded (the retry re-executes).
      schema:
        type: string
        minLength: 1
        maxLength: 255
        pattern: ^[\x21-\x7e]{1,255}$
  schemas:
    CampaignTransition:
      type: object
      required:
        - object
        - id
        - status
        - launched
      description: |
        Result of a deterministic lifecycle transition
        (`launch` / `send` / `pause` / `resume`). Carries the transition
        facts in exactly one of `launch` / `pause` / `resume` depending on
        which transition ran.
      properties:
        object:
          type: string
          const: campaign
        id:
          type: string
          format: uuid
        status:
          type: string
          enum:
            - draft
            - active
            - paused
        launched:
          type: integer
          description: Drafts scheduled by this transition (0 unless launching/resuming).
        launch:
          type: object
          description: Present on a launch/resume that scheduled work.
          required:
            - scheduled
            - firstScheduledAt
            - missingRecipientCount
            - duplicateActiveCancelledCount
            - duplicatePriorCancelledCount
          properties:
            scheduled:
              type: integer
            firstScheduledAt:
              type: string
              format: date-time
              nullable: true
            missingRecipientCount:
              type: integer
            duplicateActiveCancelledCount:
              type: integer
            duplicatePriorCancelledCount:
              type: integer
            blocked:
              type: object
              required:
                - reason
                - message
                - missingChannels
              properties:
                reason:
                  type: string
                message:
                  type: string
                missingChannels:
                  type: array
                  items:
                    type: string
        pause:
          type: object
          description: Present on a pause.
          required:
            - stoppedSequences
            - haltedSteps
            - inFlightSending
            - alreadyPaused
          properties:
            stoppedSequences:
              type: integer
            haltedSteps:
              type: integer
            inFlightSending:
              type: integer
            alreadyPaused:
              type: boolean
        resume:
          type: object
          description: Present on a resume.
          required:
            - resumedSequences
            - noAccountSequences
            - missingChannels
          properties:
            resumedSequences:
              type: integer
            noAccountSequences:
              type: integer
            missingChannels:
              type: array
              items:
                type: string
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: og_live_...

````