> ## 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.

# Delete a project (two-step; ?confirm=true, ?dryRun=true)

> Deletes the project (child org) and cascades across its whole
entity tree. Follows the v2 deletes convention — deliberately
two-step:

1. Without `?confirm=true` (or with `?dryRun=true`) → HTTP 200
   with the impact preview
   `{ id, name, confirmationRequired: true, willDelete: { workspaces, tables, rows } }`;
   nothing removed.
2. With `?confirm=true` → deletes and returns
   `{ id, name, deleted: true }`.




## OpenAPI

````yaml /openapi-v2.yaml delete /projects/{projectId}
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:
  /projects/{projectId}:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
    delete:
      tags:
        - Projects
      summary: Delete a project (two-step; ?confirm=true, ?dryRun=true)
      description: |
        Deletes the project (child org) and cascades across its whole
        entity tree. Follows the v2 deletes convention — deliberately
        two-step:

        1. Without `?confirm=true` (or with `?dryRun=true`) → HTTP 200
           with the impact preview
           `{ id, name, confirmationRequired: true, willDelete: { workspaces, tables, rows } }`;
           nothing removed.
        2. With `?confirm=true` → deletes and returns
           `{ id, name, deleted: true }`.
      operationId: deleteProject
      parameters:
        - in: query
          name: confirm
          description: Must be `true` to actually delete. Omit for the impact preview.
          schema:
            type: boolean
            default: false
        - in: query
          name: dryRun
          description: Force the impact preview even when confirm is set.
          schema:
            type: boolean
            default: false
      responses:
        '200':
          description: >-
            The impact preview (unconfirmed / dryRun) or the delete receipt
            (confirmed).
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/ProjectDeletePreview'
                  - type: object
                    required:
                      - id
                      - name
                      - deleted
                    properties:
                      id:
                        type: string
                        format: uuid
                      name:
                        type: string
                      deleted:
                        type: boolean
                        const: true
        '404':
          $ref: '#/components/responses/ProjectNotFound'
components:
  parameters:
    ProjectId:
      in: path
      name: projectId
      required: true
      description: The project (child org) id.
      schema:
        type: string
        format: uuid
  schemas:
    ProjectDeletePreview:
      type: object
      required:
        - id
        - name
        - confirmationRequired
        - willDelete
      description: |
        Impact preview returned by `DELETE /projects/{projectId}` when
        `?confirm=true` is absent (or `?dryRun=true` is set). Nothing
        has been removed.
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        confirmationRequired:
          type: boolean
          const: true
        willDelete:
          type: object
          required:
            - workspaces
            - tables
            - rows
          properties:
            workspaces:
              type: integer
            tables:
              type: integer
            rows:
              type: integer
    ErrorEnvelope:
      type: object
      required:
        - error
        - code
      properties:
        error:
          type: string
        code:
          type: string
        details:
          type: object
          additionalProperties: true
        handoff:
          type: object
          description: |
            Present on 4xx errors the caller's user can resolve in-app — a
            forwardable link on the canonical app origin.
          required:
            - kind
            - url
            - label
          properties:
            kind:
              type: string
            url:
              type: string
              format: uri
            label:
              type: string
  responses:
    ProjectNotFound:
      description: >-
        Project not found (unknown, cross-parent, or deleted — no existence
        leak)
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/ErrorEnvelope'
              - example:
                  error: Project not found
                  code: PROJECT_NOT_FOUND
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: og_live_...

````