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

# Purge subject or organization data (two-phase)

> Two-phase purge.

Two-phase purge. Phase 1 (`dryRun`, the default) counts and mints a
confirmToken; phase 2 (`dryRun: false` + that token) deletes.

Scope `subject` erases one provisioned subject completely (the GDPR
path). Scope `org` empties the organization's knowledge graph but keeps
the organization, its users and its API keys: use `DELETE /v1/me` to
delete the account itself.

A confirmed purge also deletes the stored files behind the rows it removes:
uploaded imports and finished export documents at both scopes, and at org
scope the trained ExoBrain as well, since it is derived from the graph being
emptied. Retrieval then falls back to the live rows until the org retrains.

Admin scope. Not replayable by `Idempotency-Key`: a repeated confirm must
present a valid, unexpired (5 minute) token bound to this org, scope,
subject, admin and the impact the dry run reported. If new data has landed
since that dry run the confirm is refused with 409
`purge_impact_changed` and nothing is deleted -- re-run the dry run,
review the new impact, confirm with the fresh token.

## Authorization

This route requires the `admin` scope.

A missing or invalid credential returns 401 [`authentication_error`](/errors/authentication_error). A valid credential without the scope returns 403 [`permission_denied`](/errors/permission_denied), and the problem body names the exact scope required.

## Headers

These are request conventions the contract does not declare as parameters, so they do not appear in the schema tables below.

| Header            | Applies        | What it does                                                                                                                                           |
| ----------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `X-Exo-API-Key`   | Required       | Your Exo API key, `exo_<env>_<token>`. `Authorization: Bearer exo_...` is accepted as an alternate.                                                    |
| `X-Exo-Subject`   | Not applicable | This route is not subject-scoped.                                                                                                                      |
| `Idempotency-Key` | Not honoured   | This route does not replay: a repeated confirm must present a fresh confirmation token, so the impact is always re-counted before anything is deleted. |
| `X-Request-Id`    | Response       | Returned on every response, including `204`s. Quote it in a support request.                                                                           |

## Success responses

| Status | Meaning             |
| ------ | ------------------- |
| `200`  | Successful Response |

## Errors

| Status | Code                                                   | When                                                           |
| ------ | ------------------------------------------------------ | -------------------------------------------------------------- |
| `400`  | [`invalid_request`](/errors/invalid_request)           | Malformed request, for example an invalid cursor.              |
| `401`  | [`authentication_error`](/errors/authentication_error) | Missing or invalid credentials.                                |
| `403`  | [`permission_denied`](/errors/permission_denied)       | Missing scope, or an unprovisioned subject selector.           |
| `404`  | [`not_found`](/errors/not_found)                       | The resource does not exist, or is not visible to this caller. |
| `409`  | [`conflict`](/errors/conflict)                         | A conflict with the current state of the resource.             |
| `422`  | [`validation_error`](/errors/validation_error)         | The request was understood but failed field validation.        |
| `429`  | [`rate_limit_exceeded`](/errors/rate_limit_exceeded)   | Rate limit exceeded. Retry after the Retry-After interval.     |
| `503`  | [`service_degraded`](/errors/service_degraded)         | A dependency is temporarily unavailable. Safe to retry.        |

Beyond the shared statuses, this route can answer with [`purge_impact_changed`](/errors/purge_impact_changed). Every problem body names its own `code`, and the `type` URI always resolves to the matching page.

Every error is an RFC 9457 `application/problem+json` body carrying a stable `code`, a `requestId`, and a `suggestedAction` where Exo has one. See [Errors](/platform/errors).

## Notes

* **Two phases, and the first one is the default.** `dryRun` true counts the impact and mints a `confirmToken`. `dryRun` false with that token deletes.
* **The token expires and the impact is re-checked.** If the data has changed since the dry run, the confirm is refused with 409 `purge_impact_changed` and nothing is deleted.
* **`scope: "org"` empties the graph but keeps the organization**, its users and its keys. To delete the account itself, use `DELETE /v1/me`.
* **A confirmed purge also deletes stored files**: uploaded imports and finished export documents at both scopes, plus the trained brain at organization scope, because it is derived from the graph being emptied.


## OpenAPI

````yaml openapi.json POST /v1/purge
openapi: 3.1.0
info:
  contact:
    name: Exo
    url: https://get-exo.com/
  description: >-
    The Exo public developer API: ingest content, retrieve identity-conditioned
    context, and read the knowledge graph. Authenticate every request with an
    Exo API key, either as the X-Exo-API-Key header or as Authorization: Bearer
    exo_... . Errors follow RFC 9457 problem+json with a stable snake_case code
    on every body.
  title: Exo API
  version: 1.0.0
servers:
  - url: https://api.get-exo.com
security:
  - ApiKeyHeader: []
  - BearerAuth: []
tags:
  - name: retrieve
    x-group: Retrieval
  - name: search
    x-group: Search
  - name: ingest
    x-group: Ingestion
  - name: jobs
    x-group: Jobs
  - name: subjects
    x-group: Subjects
  - name: recall
    x-group: Recall
  - name: graph
    x-group: Graph
  - name: insights
    x-group: Contradictions and proposals
  - name: condition
    x-group: Condition
  - name: brain
    x-group: Brain
  - name: sessions
    x-group: Coding sessions
  - name: connectors
    x-group: Connectors
  - name: events
    x-group: Events and webhooks
  - name: meta
    x-group: Identity
  - name: keys
    x-group: Keys and sandbox
  - name: usage
    x-group: Usage
  - name: settings
    x-group: Settings
  - name: exports
    x-group: Exports
  - name: team
    x-group: Team
  - name: admin
    x-group: Account and purge
paths:
  /v1/purge:
    post:
      tags:
        - admin
      summary: Purge subject or organization data (two-phase)
      description: >-
        Two-phase purge. Phase 1 (``dryRun``, the default) counts and mints a

        confirmToken; phase 2 (``dryRun: false`` + that token) deletes.


        Scope ``subject`` erases one provisioned subject completely (the GDPR

        path). Scope ``org`` empties the organization's knowledge graph but
        keeps

        the organization, its users and its API keys: use ``DELETE /v1/me`` to

        delete the account itself.


        A confirmed purge also deletes the stored files behind the rows it
        removes:

        uploaded imports and finished export documents at both scopes, and at
        org

        scope the trained ExoBrain as well, since it is derived from the graph
        being

        emptied. Retrieval then falls back to the live rows until the org
        retrains.


        Admin scope. Not replayable by ``Idempotency-Key``: a repeated confirm
        must

        present a valid, unexpired (5 minute) token bound to this org, scope,

        subject, admin AND the impact the dry run reported. If new data has
        landed

        since that dry run the confirm is refused with 409

        ``purge_impact_changed`` and nothing is deleted -- re-run the dry run,

        review the new impact, confirm with the fresh token.
      operationId: purgeData
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PurgeRequest'
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PurgeResponse'
          description: Successful Response
        '400':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
          description: Malformed request, for example an invalid cursor.
        '401':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
          description: Missing or invalid credentials
        '403':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
          description: Missing scope or unprovisioned subject
        '404':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
          description: The subject is not provisioned
        '409':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
          description: The impact changed since the dry run, or the mapping is corrupt
        '422':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
          description: Malformed request or invalid confirmation token
        '429':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
          description: Rate limit exceeded
        '503':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
          description: A dependency is unavailable
components:
  schemas:
    PurgeRequest:
      description: >-
        Body of POST /v1/purge.


        ``subjectId`` is the OBJECT of the call, not a subject selector (this

        family runs without the subject seam, exactly like /v1/subjects), which
        is

        why it is not named ``subject``.
      properties:
        confirmToken:
          anyOf:
            - maxLength: 4096
              type: string
            - type: 'null'
          description: The confirmToken from the dry run. Required when dryRun is false.
          title: Confirmtoken
        dryRun:
          default: true
          description: >-
            Default true: count the impact and mint a confirmToken, delete
            nothing.
          title: Dryrun
          type: boolean
        scope:
          enum:
            - subject
            - org
          title: Scope
          type: string
        subjectId:
          anyOf:
            - maxLength: 128
              type: string
            - type: 'null'
          description: >-
            Subject to erase. Required for scope=subject, rejected for
            scope=org.
          title: Subjectid
      required:
        - scope
      title: PurgeRequest
      type: object
    PurgeResponse:
      description: >-
        Both phases of the ceremony share one shape.


        Phase 1 fills ``confirmToken``/``expiresAt``; phase 2 fills ``purgedAt``

        and reports ACTUAL per-table deleted counts under the same ``counts``
        keys.

        Zero-row tables are omitted from ``counts`` so the payload stays
        readable;

        ``totalRows`` is the sum of everything reported.
      properties:
        confirmToken:
          anyOf:
            - type: string
            - type: 'null'
          title: Confirmtoken
        counts:
          additionalProperties:
            type: integer
          title: Counts
          type: object
        dryRun:
          title: Dryrun
          type: boolean
        expiresAt:
          anyOf:
            - format: date-time
              type: string
            - type: 'null'
          title: Expiresat
        preserved:
          anyOf:
            - additionalProperties:
                type: integer
              type: object
            - type: 'null'
          title: Preserved
        purgedAt:
          anyOf:
            - format: date-time
              type: string
            - type: 'null'
          title: Purgedat
        requestId:
          title: Requestid
          type: string
        scope:
          title: Scope
          type: string
        skipped:
          anyOf:
            - items:
                type: string
              type: array
            - type: 'null'
          description: >-
            Tables this purge could not clear (absent from an older schema, or
            blocked by a foreign key). Present only when non-empty: a purge that
            left rows behind must never look identical to one that had none.
          title: Skipped
        subjectId:
          anyOf:
            - type: string
            - type: 'null'
          title: Subjectid
        totalRows:
          title: Totalrows
          type: integer
      required:
        - requestId
        - scope
        - dryRun
        - counts
        - totalRows
      title: PurgeResponse
      type: object
    Problem:
      description: |-
        RFC 9457 problem+json envelope (spec D9), documented in OpenAPI.

        Invariant: ``code`` equals the tail of the ``type`` URI
        (``https://docs.get-exo.com/errors/<code>``).
      properties:
        code:
          title: Code
          type: string
        detail:
          title: Detail
          type: string
        documentationUrl:
          anyOf:
            - type: string
            - type: 'null'
          title: Documentationurl
        errors:
          anyOf:
            - items:
                $ref: '#/components/schemas/FieldViolation'
              type: array
            - type: 'null'
          title: Errors
        requestId:
          default: ''
          title: Requestid
          type: string
        status:
          title: Status
          type: integer
        suggestedAction:
          anyOf:
            - type: string
            - type: 'null'
          title: Suggestedaction
        title:
          title: Title
          type: string
        type:
          title: Type
          type: string
      required:
        - type
        - title
        - status
        - detail
        - code
      title: Problem
      type: object
    FieldViolation:
      description: A single field-level violation inside a Problem ``errors[]`` list.
      properties:
        code:
          title: Code
          type: string
        field:
          title: Field
          type: string
        message:
          title: Message
          type: string
        pointer:
          title: Pointer
          type: string
      required:
        - pointer
        - field
        - code
        - message
      title: FieldViolation
      type: object
  securitySchemes:
    ApiKeyHeader:
      description: An Exo API key (exo_...) sent as the X-Exo-API-Key header.
      in: header
      name: X-Exo-API-Key
      type: apiKey
    BearerAuth:
      description: The same Exo API key (exo_...) sent as an Authorization bearer token.
      scheme: bearer
      type: http

````