openapi: 3.0.3
info:
  title: "DealArena API & MCP Docs"
  version: "1.1.0"
  description: |
    REST API v1 for DealArena — drive your whole desk through `/api/v1`, or
    connect Claude through the DealArena MCP server (below).

    ## Authentication
    Every endpoint requires a bearer token minted in **Settings › API Access**
    (`da_live_...`). Tokens are scoped; each operation lists its required scope
    (`x-required-scope`). `GET /me` works with any valid token.

    ## Envelope
    Success: `{"data": ..., "meta": {...}?}` — list endpoints carry
    `meta.limit / meta.offset / meta.total`.
    Error: `{"error": {"code": "<snake_code>", "message": "<human sentence>"}}`.
    The nine codes in `#/components/schemas/Error` are the only error shapes
    the API emits.

    ## Rate limits & quotas
    Burst: 60 requests / 60s rolling per token (list endpoints weigh
    `ceil(limit/10)`), reported via `X-RateLimit-*`. Daily: a durable per-token
    quota (default 5,000/day Pro, 20,000/day Enterprise) that rolls at midnight
    UTC, reported via `X-RateLimit-Daily-*`. Exceeding either returns 429
    (`rate_limited` vs `quota_exceeded`) with `Retry-After`.

    ## Tier
    The token owner's tier is resolved on every request. Below Pro → 403
    `tier_required`. Credit-metered endpoints
    (`/outreach/draft`, `/agents/ask`) charge the same credits as the UI.

    ## MCP Server
    `dealarena-mcp` is an npm package that runs a local MCP (Model Context
    Protocol) server over this API, exposing 19 workflow tools to Claude
    Desktop, Claude Code, and any MCP client. Local stdio transport only:
    your token stays on your machine, and tier, scopes, and revocation are
    enforced server-side on every call.

    ### Requirements
    - Node.js ≥ 18
    - A Pro account
    - An API token (`da_live_...`) minted in **Settings › API Access**

    ### Connect
    Claude Code:

    ```bash
    claude mcp add dealarena -- npx dealarena-mcp
    ```

    Claude Desktop, in `claude_desktop_config.json`:

    ```json
    {
      "mcpServers": {
        "dealarena": {
          "command": "npx",
          "args": ["dealarena-mcp"],
          "env": { "DEALARENA_API_KEY": "da_live_..." }
        }
      }
    }
    ```

    The server registers under the name `dealarena`.

    ### Environment variables
    | Variable | Required | Default | Notes |
    |---|---|---|---|
    | `DEALARENA_API_KEY` | yes | (none) | Your `da_live_...` token from Settings › API Access |
    | `DEALARENA_API_URL` | no | `https://app.dealarena.io` | Non-localhost values must be HTTPS |

    ### Tools
    | Tool | What it does | Scope | Notes |
    |---|---|---|---|
    | `search_prospects` | Search prospects by query or stage | `prospects:read` | Read-only; up to 25 rows |
    | `get_prospect` | Fetch one prospect | `prospects:read` | Read-only |
    | `create_prospect` | Create a prospect | `prospects:write` | Name plus optional company, title, email, phone, LinkedIn, notes |
    | `create_prospects_bulk` | Bulk-create prospects | `prospects:write` | Up to 100 per call; per-row errors reported |
    | `update_prospect` | Update prospect fields | `prospects:write` | Idempotent |
    | `lookup_company` | Find a company by name or id | `companies:read` | Read-only |
    | `log_activity` | Log a call, meeting, note, or email | `activities:write` | Awards XP |
    | `move_deal_stage` | Move a deal through the pipeline | `deals:write` | Awards XP; shows in the UI immediately |
    | `list_my_tasks` | List your tasks | `tasks:read` | Read-only; filters: status, due |
    | `create_task` | Create a task | `tasks:write` | Optional due date and prospect link |
    | `complete_task` | Complete a task | `tasks:write` | Idempotent |
    | `trash_prospect` | Move a prospect to Trash | `prospects:write` | Recoverable: Trash only, no hard delete |
    | `draft_outreach` | Draft an email, LinkedIn message, or call opener | `outreach:write` | Costs 1 AI credit |
    | `list_sequences` | List your sequences | `sequences:read` | Read-only |
    | `enroll_in_sequence` | Enroll a prospect in a sequence | `sequences:write` | Caution: fires real outbound touches |
    | `get_pipeline_summary` | Per-stage counts, TCV, deltas | `analytics:read` | Read-only; period: 7d, 30d, or 90d |
    | `get_my_stats` | Identity, tier, credits, XP, streak | any token | Read-only |
    | `get_sauce_signals` | Sauce feed: market, news, or leads | `signals:read` | Read-only |
    | `ask_agent` | Ask the BDM Legion agent | `agents:invoke` | Costs 5 credits; agent: `legion` only |

    ### Behavior
    Requests time out at 30s (60s for `draft_outreach` and `ask_agent`).
    One automatic retry on 429, honoring `Retry-After`. List tools cap at
    25 rows. Errors surface this API's nine-code dialect (above) with fixed,
    human-readable messages. For reporting-only setups, mint a Read-only
    token in Settings › API Access.
servers:
  - url: https://app.dealarena.io/api/v1
security:
  - BearerAuth: []
tags:
  - name: identity
  - name: prospects
  - name: deals
  - name: activities
  - name: tasks
  - name: analytics
  - name: outreach
  - name: companies
  - name: sauce
  - name: sequences
  - name: agents
  - name: legion
  - name: webhooks

paths:
  /me:
    get:
      tags: [identity]
      summary: Who am I — identity, tier, credits, XP, streak, token scopes
      description: Works with ANY valid token (no scope required). Cheap; MCP clients call it for context.
      x-required-scope: none
      responses:
        '200':
          description: Caller identity
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id: { type: integer }
                      name: { type: string }
                      email: { type: string }
                      tier: { type: string, enum: [free, solo, pro, enterprise] }
                      credits:
                        type: object
                        properties:
                          allotment: { type: integer }
                          used_this_month: { type: integer }
                          remaining: { type: integer }
                      xp_total: { type: integer }
                      streak:
                        type: object
                        properties:
                          current: { type: integer }
                          longest: { type: integer }
                      scopes:
                        type: array
                        items: { type: string }
        '401': { $ref: '#/components/responses/AuthRequired' }
        '403': { $ref: '#/components/responses/TierRequired' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /prospects:
    get:
      tags: [prospects]
      summary: List/search prospects
      x-required-scope: prospects:read
      parameters:
        - { name: q, in: query, schema: { type: string }, description: "ILIKE search over name/company/title/email" }
        - { name: stage, in: query, schema: { type: string }, description: "Filter by pipeline stage key (alias: status)" }
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200': { $ref: '#/components/responses/ProspectList' }
        '401': { $ref: '#/components/responses/AuthRequired' }
        '403': { $ref: '#/components/responses/InsufficientScope' }
        '429': { $ref: '#/components/responses/RateLimited' }
    post:
      tags: [prospects]
      summary: Create a prospect
      x-required-scope: prospects:write
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ProspectInput' }
      responses:
        '201': { $ref: '#/components/responses/ProspectSingle' }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '401': { $ref: '#/components/responses/AuthRequired' }
        '403': { $ref: '#/components/responses/InsufficientScope' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /prospects/bulk:
    post:
      tags: [prospects]
      summary: Bulk-create up to 100 prospects
      description: Per-item results — failed rows are reported by index, valid rows are still created. Awards no XP (parity with CSV import).
      x-required-scope: prospects:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - type: array
                  items: { $ref: '#/components/schemas/ProspectInput' }
                - type: object
                  properties:
                    prospects:
                      type: array
                      items: { $ref: '#/components/schemas/ProspectInput' }
      responses:
        '201':
          description: Per-item results
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      created: { type: integer }
                      failed: { type: integer }
                      results:
                        type: array
                        items:
                          type: object
                          properties:
                            index: { type: integer }
                            id: { type: string }
                            error: { $ref: '#/components/schemas/ErrorBody' }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '401': { $ref: '#/components/responses/AuthRequired' }
        '403': { $ref: '#/components/responses/InsufficientScope' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /prospects/{pid}:
    parameters:
      - { name: pid, in: path, required: true, schema: { type: string } }
    get:
      tags: [prospects]
      summary: Get one prospect
      x-required-scope: prospects:read
      responses:
        '200': { $ref: '#/components/responses/ProspectSingle' }
        '401': { $ref: '#/components/responses/AuthRequired' }
        '403': { $ref: '#/components/responses/InsufficientScope' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
    put:
      tags: [prospects]
      summary: Update prospect fields
      x-required-scope: prospects:write
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ProspectInput' }
      responses:
        '200': { $ref: '#/components/responses/ProspectSingle' }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '401': { $ref: '#/components/responses/AuthRequired' }
        '403': { $ref: '#/components/responses/InsufficientScope' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
    delete:
      tags: [prospects]
      summary: Move a prospect to the Trash (soft delete)
      description: Sets `deleted_at` — the prospect lands in the in-app Trash (/database/trash), restores cleanly, and is hard-purged only by the 30-day worker. No API code path hard-deletes.
      x-required-scope: prospects:write
      responses:
        '200':
          description: Trashed
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id: { type: string }
                      trashed: { type: boolean }
        '401': { $ref: '#/components/responses/AuthRequired' }
        '403': { $ref: '#/components/responses/InsufficientScope' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /prospects/{pid}/stage:
    patch:
      tags: [deals]
      summary: Move a prospect through the pipeline
      description: Same validation, XP award, activity log, and webhook emission as the UI kanban drag. Honors the tenant's custom pipeline stage keys; an invalid key returns `validation_failed` naming the valid keys.
      x-required-scope: deals:write
      parameters:
        - { name: pid, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [stage]
              properties:
                stage: { type: string, example: qualified }
      responses:
        '200':
          description: Stage moved
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id: { type: string }
                      stage: { type: string }
                      previous_stage: { type: string }
                      xp_awarded: { type: integer }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '401': { $ref: '#/components/responses/AuthRequired' }
        '403': { $ref: '#/components/responses/InsufficientScope' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /deals:
    get:
      tags: [deals]
      summary: List deals (prospects in deal-stage statuses)
      x-required-scope: deals:read
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200': { $ref: '#/components/responses/ProspectList' }
        '401': { $ref: '#/components/responses/AuthRequired' }
        '403': { $ref: '#/components/responses/InsufficientScope' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /deals/{pid}:
    parameters:
      - { name: pid, in: path, required: true, schema: { type: string } }
    get:
      tags: [deals]
      summary: Get one deal
      x-required-scope: deals:read
      responses:
        '200': { $ref: '#/components/responses/ProspectSingle' }
        '401': { $ref: '#/components/responses/AuthRequired' }
        '403': { $ref: '#/components/responses/InsufficientScope' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
    put:
      tags: [deals]
      summary: Update deal fields
      x-required-scope: deals:write
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ProspectInput' }
      responses:
        '200': { $ref: '#/components/responses/ProspectSingle' }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '401': { $ref: '#/components/responses/AuthRequired' }
        '403': { $ref: '#/components/responses/InsufficientScope' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /activities:
    post:
      tags: [activities]
      summary: Log an activity (call / meeting / note / email)
      description: Writes the identical `activity_log` row shape the UI writes, so profile feeds and the connected-call filter pick API rows up. Awards no XP (UI parity).
      x-required-scope: activities:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [prospect_id, type, summary]
              properties:
                prospect_id: { type: string }
                type: { type: string, enum: [call, meeting, note, email] }
                summary: { type: string }
                outcome:
                  type: string
                  description: "Per-type vocabulary. call: connected|voicemail|no_answer|bad_number|callback|qualified|disqualified · email: sent|replied|bounced|no_response · meeting: held|booked|no_show|rescheduled · note: (none)"
      responses:
        '201':
          description: Logged
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      prospect_id: { type: string }
                      type: { type: string }
                      outcome: { type: string, nullable: true }
                      logged: { type: boolean }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '401': { $ref: '#/components/responses/AuthRequired' }
        '403': { $ref: '#/components/responses/InsufficientScope' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /tasks:
    get:
      tags: [tasks]
      summary: List my tasks
      description: User-scoped (the token owner's tasks). The in-app list view is workspace-shared; this endpoint is deliberately "my tasks".
      x-required-scope: tasks:read
      parameters:
        - { name: status, in: query, schema: { type: string, enum: [open, done, all], default: all } }
        - { name: due, in: query, schema: { type: string, enum: [today, overdue, all], default: all } }
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200':
          description: Tasks
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Task' }
                  meta: { $ref: '#/components/schemas/ListMeta' }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '401': { $ref: '#/components/responses/AuthRequired' }
        '403': { $ref: '#/components/responses/InsufficientScope' }
        '429': { $ref: '#/components/responses/RateLimited' }
    post:
      tags: [tasks]
      summary: Create a task
      description: Awards the same `task_added` XP as the UI.
      x-required-scope: tasks:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title]
              properties:
                title: { type: string }
                due_date: { type: string, example: "2026-07-25" }
                prospect_id: { type: string }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id: { type: string }
                      title: { type: string }
                      due_date: { type: string, nullable: true }
                      prospect_id: { type: string, nullable: true }
                      xp_awarded: { type: integer }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '401': { $ref: '#/components/responses/AuthRequired' }
        '403': { $ref: '#/components/responses/InsufficientScope' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /tasks/{task_id}:
    patch:
      tags: [tasks]
      summary: Complete (or reopen) a task
      description: "`{\"done\": true}` completes; XP (`task_completed`) fires only on the open→completed transition — idempotent replays award nothing."
      x-required-scope: tasks:write
      parameters:
        - { name: task_id, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [done]
              properties:
                done: { type: boolean }
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id: { type: string }
                      done: { type: boolean }
                      xp_awarded: { type: integer }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '401': { $ref: '#/components/responses/AuthRequired' }
        '403': { $ref: '#/components/responses/InsufficientScope' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /pipeline/summary:
    get:
      tags: [analytics]
      summary: Pipeline summary — per-stage counts, TCV totals, deltas, stale callouts
      x-required-scope: analytics:read
      parameters:
        - { name: period, in: query, schema: { type: string, enum: [7d, 30d, 90d], default: 30d } }
      responses:
        '200':
          description: Summary
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      period: { type: string }
                      currency_metric: { type: string, example: tcv }
                      stages:
                        type: array
                        items:
                          type: object
                          properties:
                            stage: { type: string }
                            count: { type: integer }
                            value: { type: number }
                            entered_period: { type: integer }
                            entered_prev_period: { type: integer }
                            delta_pct: { type: number, nullable: true }
                      totals:
                        type: object
                        properties:
                          open_count: { type: integer }
                          open_value: { type: number }
                          won_count: { type: integer }
                          won_value: { type: number }
                      oldest_untouched:
                        type: array
                        maxItems: 3
                        items:
                          type: object
                          properties:
                            id: { type: string }
                            name: { type: string }
                            company: { type: string }
                            stage: { type: string }
                            deal_size: { type: number, nullable: true }
                            days_untouched: { type: integer, nullable: true }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '401': { $ref: '#/components/responses/AuthRequired' }
        '403': { $ref: '#/components/responses/InsufficientScope' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /outreach/draft:
    post:
      tags: [outreach]
      summary: Draft outreach for a prospect (1 credit)
      description: Charges exactly 1 credit through the same meter as the UI — token traffic gets zero discount. At zero credits returns 402 `credits_exhausted` and burns nothing. Output capped at 300 tokens.
      x-required-scope: outreach:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [prospect_id, channel]
              properties:
                prospect_id: { type: string }
                channel: { type: string, enum: [email, linkedin, call_opener] }
                instructions: { type: string, maxLength: 1000 }
      responses:
        '200':
          description: Draft
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      prospect_id: { type: string }
                      channel: { type: string }
                      draft: { type: string }
                      credits_charged: { type: integer }
                      credits_remaining: { type: integer }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '401': { $ref: '#/components/responses/AuthRequired' }
        '402': { $ref: '#/components/responses/CreditsExhausted' }
        '403': { $ref: '#/components/responses/InsufficientScope' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /companies:
    get:
      tags: [companies]
      summary: List/search companies (accounts)
      x-required-scope: companies:read
      parameters:
        - { name: q, in: query, schema: { type: string }, description: "ILIKE search over name/domain/normalized name" }
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200':
          description: Companies with prospect_count + total_deal_value aggregates
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Company' }
                  meta: { $ref: '#/components/schemas/ListMeta' }
        '401': { $ref: '#/components/responses/AuthRequired' }
        '403': { $ref: '#/components/responses/InsufficientScope' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /companies/{account_id}:
    get:
      tags: [companies]
      summary: Get one company + its linked prospects
      x-required-scope: companies:read
      parameters:
        - { name: account_id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          description: Company with `prospects` array
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/Company' }
        '401': { $ref: '#/components/responses/AuthRequired' }
        '403': { $ref: '#/components/responses/InsufficientScope' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /sauce:
    get:
      tags: [sauce]
      summary: The Sauce — market/news signals + Daily Sauce Leads
      description: "`feed=market` (SEC/Polymarket events) · `feed=news` (GNews mentions) · `feed=leads` (latest Daily Sauce Leads snapshot)."
      x-required-scope: signals:read
      parameters:
        - { name: feed, in: query, schema: { type: string, enum: [market, news, leads], default: market } }
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200':
          description: Feed payload (signals for market/news; snapshot for leads)
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: object }
                  meta: { $ref: '#/components/schemas/ListMeta' }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '401': { $ref: '#/components/responses/AuthRequired' }
        '403': { $ref: '#/components/responses/InsufficientScope' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /sequences:
    get:
      tags: [sequences]
      summary: List my sequences
      x-required-scope: sequences:read
      responses:
        '200':
          description: Sequences (id, name, active, step_count)
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: integer }
                        name: { type: string }
                        description: { type: string, nullable: true }
                        active: { type: boolean }
                        step_count: { type: integer }
                        created_at: { type: string }
        '401': { $ref: '#/components/responses/AuthRequired' }
        '403': { $ref: '#/components/responses/InsufficientScope' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /sequences/{sid}/enroll:
    post:
      tags: [sequences]
      summary: Enroll a prospect in a sequence
      description: Identical to in-app enrollment — tier gate (free tier is refused), solo cap, start-node resolution, executor signal, and the `sequence.enrolled` trigger. Re-enrolling an active enrollment returns `reused true`.
      x-required-scope: sequences:write
      parameters:
        - { name: sid, in: path, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [prospect_id]
              properties:
                prospect_id: { type: string }
      responses:
        '201':
          description: Enrolled
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      enrollment_id: { type: integer }
                      reused: { type: boolean }
        '200':
          description: Already actively enrolled (reused)
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      enrollment_id: { type: integer }
                      reused: { type: boolean }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '401': { $ref: '#/components/responses/AuthRequired' }
        '403': { $ref: '#/components/responses/TierRequired' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /agents/ask:
    post:
      tags: [agents]
      summary: Ask an agent (launch allowlist — legion, 5 credits)
      description: "BDM Legion is the first agent behind this endpoint. `context` may pin a `member_id`, `prospect_id`, or one of Legion's task types (research | draft_email | follow_up | objection_handling | call_prep); otherwise your first active member free-runs the prompt. An unknown agent returns `validation_failed` naming the available agents."
      x-required-scope: agents:invoke
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [agent, prompt]
              properties:
                agent: { type: string, enum: [legion] }
                prompt: { type: string }
                context:
                  type: object
                  properties:
                    member_id: { type: integer }
                    prospect_id: { type: string }
                    task_type: { type: string, enum: [research, draft_email, follow_up, objection_handling, call_prep] }
      responses:
        '200':
          description: Agent output
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      agent: { type: string }
                      member_id: { type: integer }
                      run_id: { type: integer, nullable: true }
                      task_type: { type: string }
                      output: { type: object }
                      credits_charged: { type: integer }
                      credits_remaining: { type: integer }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '401': { $ref: '#/components/responses/AuthRequired' }
        '402': { $ref: '#/components/responses/CreditsExhausted' }
        '403': { $ref: '#/components/responses/InsufficientScope' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /legion:
    get:
      tags: [legion]
      summary: List Legion members
      x-required-scope: legion:read
      responses:
        '200':
          description: Members
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/LegionMember' }
        '401': { $ref: '#/components/responses/AuthRequired' }
        '403': { $ref: '#/components/responses/InsufficientScope' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /legion/{mid}:
    get:
      tags: [legion]
      summary: Get one Legion member
      x-required-scope: legion:read
      parameters:
        - { name: mid, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          description: Member
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/LegionMember' }
        '401': { $ref: '#/components/responses/AuthRequired' }
        '403': { $ref: '#/components/responses/InsufficientScope' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /webhooks:
    get:
      tags: [webhooks]
      summary: List webhook subscriptions
      x-required-scope: manage:webhooks
      responses:
        '200':
          description: Subscriptions
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/WebhookSubscription' }
        '401': { $ref: '#/components/responses/AuthRequired' }
        '403': { $ref: '#/components/responses/InsufficientScope' }
        '429': { $ref: '#/components/responses/RateLimited' }
    post:
      tags: [webhooks]
      summary: Create a webhook subscription
      description: >
        The signing `secret` is returned once at creation. Deliveries are signed
        `X-DealArena-Signature: t=...,v1=...` (HMAC-SHA256).


        The `url` must be `https://` on a publicly routable host. Private,
        loopback, link-local and cloud-metadata addresses are rejected at
        registration and again at delivery time, so a hostname that later
        resolves to one of them is dropped rather than delivered. Redirects are
        not followed — respond `2xx` at the URL you register.
      x-required-scope: manage:webhooks
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, events]
              properties:
                url:
                  type: string
                  format: uri
                  description: HTTPS endpoint on a publicly routable host.
                  example: https://hooks.example.com/dealarena
                events:
                  type: array
                  items:
                    type: string
                    enum: [prospect.created, prospect.updated, prospect.confidential_changed, deal.stage_changed, deal.closed_won, deal.closed_lost, signal.fired, legion.run_completed, enrichment.completed]
      responses:
        '201':
          description: Created (secret shown once)
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/WebhookSubscription' }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '401': { $ref: '#/components/responses/AuthRequired' }
        '403': { $ref: '#/components/responses/InsufficientScope' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /webhooks/{wid}:
    delete:
      tags: [webhooks]
      summary: Revoke a webhook subscription
      x-required-scope: manage:webhooks
      parameters:
        - { name: wid, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          description: Revoked
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id: { type: integer }
                      revoked: { type: boolean }
        '401': { $ref: '#/components/responses/AuthRequired' }
        '403': { $ref: '#/components/responses/InsufficientScope' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: "da_live_* personal access token (Settings › API Access)"

  parameters:
    Limit:
      name: limit
      in: query
      schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
      description: Page size. List requests over 10 items weigh ceil(limit/10) against the burst limit.
    Offset:
      name: offset
      in: query
      schema: { type: integer, minimum: 0, default: 0 }

  headers:
    RetryAfter:
      schema: { type: integer }
      description: Seconds until the limit resets (burst window, or midnight UTC for the daily quota).
    XRateLimitLimit:
      schema: { type: integer }
      description: Burst limit per 60s window (60), or the daily limit on quota_exceeded responses.
    XRateLimitRemaining:
      schema: { type: integer }
    XRateLimitDailyLimit:
      schema: { type: integer }
      description: Durable daily quota for the token owner's tier.
    XRateLimitDailyRemaining:
      schema: { type: integer }

  schemas:
    ErrorBody:
      type: object
      required: [code, message]
      properties:
        code:
          type: string
          enum: [auth_required, insufficient_scope, tier_required, validation_failed, not_found, rate_limited, quota_exceeded, credits_exhausted, server_error]
        message: { type: string }
    Error:
      type: object
      required: [error]
      properties:
        error: { $ref: '#/components/schemas/ErrorBody' }
    ListMeta:
      type: object
      properties:
        limit: { type: integer }
        offset: { type: integer }
        total: { type: integer }
    ProspectInput:
      type: object
      properties:
        name: { type: string }
        company: { type: string }
        title: { type: string }
        email: { type: string }
        phone: { type: string }
        status: { type: string }
        deal_size: { type: number }
        source: { type: string, default: api }
        linkedin_url: { type: string }
        notes: { type: string }
        confidential: { type: boolean, default: false }
    Prospect:
      allOf:
        - $ref: '#/components/schemas/ProspectInput'
        - type: object
          properties:
            id: { type: string }
            user_id: { type: integer }
            created_at: { type: string }
            status_updated_at: { type: string, nullable: true }
    Task:
      type: object
      properties:
        id: { type: string }
        title: { type: string }
        description: { type: string, nullable: true }
        due_date: { type: string, nullable: true }
        status: { type: string, description: "pending | completed" }
        priority: { type: string }
        category: { type: string }
        created_at: { type: string }
        prospect_id: { type: string, nullable: true }
        prospect_name: { type: string, nullable: true }
        prospect_company: { type: string, nullable: true }
    Company:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        domain: { type: string, nullable: true }
        industry: { type: string, nullable: true }
        headcount_band: { type: string, nullable: true }
        revenue_band: { type: string, nullable: true }
        technologies: { type: string, nullable: true }
        hiring: { type: boolean, nullable: true }
        target_status: { type: string, nullable: true }
        prospect_count: { type: integer }
        total_deal_value: { type: number }
        prospects:
          type: array
          items: { $ref: '#/components/schemas/Prospect' }
          description: Present on the single-company response only.
    LegionMember:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        archetype: { type: string }
        avatar_url: { type: string, nullable: true }
        active: { type: boolean }
        attributed_deals: { type: integer, description: 'Computed from won deals on assigned prospects/accounts' }
        attributed_revenue_cents: { type: integer, description: 'Computed; USD deals only, dollars x100' }
        created_at: { type: string }
        updated_at: { type: string, nullable: true }
    WebhookSubscription:
      type: object
      properties:
        id: { type: integer }
        url: { type: string }
        events:
          type: array
          items: { type: string }
        active: { type: boolean }
        created_at: { type: string }
        revoked_at: { type: string, nullable: true }
        secret: { type: string, description: Returned once at creation only. }

  responses:
    ProspectList:
      description: Prospect page
      content:
        application/json:
          schema:
            type: object
            properties:
              data:
                type: array
                items: { $ref: '#/components/schemas/Prospect' }
              meta: { $ref: '#/components/schemas/ListMeta' }
    ProspectSingle:
      description: One prospect
      content:
        application/json:
          schema:
            type: object
            properties:
              data: { $ref: '#/components/schemas/Prospect' }
    AuthRequired:
      description: Missing/invalid/revoked token, or the owner account is deleted (auth_required)
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    InsufficientScope:
      description: Token lacks the required scope (insufficient_scope)
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    TierRequired:
      description: Token owner's tier is below the requirement (tier_required)
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    ValidationFailed:
      description: Bad input (validation_failed)
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: No such resource for this token's owner (not_found)
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    CreditsExhausted:
      description: Monthly credit allotment used up (credits_exhausted) — nothing was charged
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    RateLimited:
      description: Burst limit (rate_limited) or daily quota (quota_exceeded) exceeded
      headers:
        Retry-After: { $ref: '#/components/headers/RetryAfter' }
        X-RateLimit-Limit: { $ref: '#/components/headers/XRateLimitLimit' }
        X-RateLimit-Remaining: { $ref: '#/components/headers/XRateLimitRemaining' }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
