openapi: 3.1.0
info:
  title: Armada API
  version: "1.0.0"
  description: |
    Workspace-scoped REST API for lists, Find, enrollments, content, and webhooks.
    Auth: `Authorization: Bearer arm_…` (Settings → Developers).
    Mutating requests accept optional `Idempotency-Key` (24h replay cache).
    Bearer-only — no browser CORS. Source of truth: Zod in `@armada/contracts` + control-plane.
    @see docs/api.md
servers:
  - url: https://app.getarmada.com
    description: Production (path prefix /api/v1)
  - url: http://localhost:3000
    description: Local web
security:
  - bearerAuth: []
tags:
  - name: me
  - name: lists
  - name: find
  - name: campaigns
  - name: enrollments
  - name: content
  - name: accounts
  - name: inventory
  - name: webhooks
  - name: inbox
  - name: billing
paths:
  /api/v1/me:
    get:
      tags: [me]
      summary: Key identity, scopes, entitlements, credits, on-demand
      operationId: getMe
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Me"
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Missing entitlement or scope
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          description: Rate limited
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/v1/billing:
    get:
      tags: [billing]
      summary: Credits + on-demand status
      operationId: getBilling
      responses:
        "200":
          description: OK
  /api/v1/billing/checkout-link:
    post:
      tags: [billing]
      summary: Stripe URL for enable_on_demand / upgrade / portal
      operationId: createBillingCheckoutLink
      responses:
        "200":
          description: OK
  /api/v1/billing/on-demand:
    patch:
      tags: [billing]
      summary: Update on-demand limit when already enabled
      operationId: patchOnDemand
      responses:
        "200":
          description: OK
  /api/v1/lists:
    get:
      tags: [lists]
      summary: List workspace lists
      operationId: listLists
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/ListSummary"
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      tags: [lists]
      summary: Create a list
      operationId: createList
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateListBody"
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ListDetail"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /api/v1/lists/{listId}:
    get:
      tags: [lists]
      summary: List detail + columns
      operationId: getList
      parameters:
        - $ref: "#/components/parameters/listId"
      responses:
        "200":
          description: OK
  /api/v1/lists/{listId}/rows:
    get:
      tags: [lists]
      summary: Page list rows
      operationId: listRows
      parameters:
        - $ref: "#/components/parameters/listId"
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 200 }
        - name: offset
          in: query
          schema: { type: integer, minimum: 0 }
        - name: search
          in: query
          schema: { type: string }
      responses:
        "200":
          description: OK
    post:
      tags: [lists]
      summary: Append rows
      operationId: appendRows
      parameters:
        - $ref: "#/components/parameters/listId"
      responses:
        "201":
          description: Created
  /api/v1/lists/{listId}/find-runs:
    post:
      tags: [find]
      summary: Start Find on an existing list
      operationId: startFindRunOnList
      parameters:
        - $ref: "#/components/parameters/listId"
      responses:
        "202":
          description: Accepted
  /api/v1/lists/{listId}/enrich:
    post:
      tags: [find]
      summary: Enqueue enrich jobs for list rows
      operationId: enrichList
      parameters:
        - $ref: "#/components/parameters/listId"
      responses:
        "202":
          description: Accepted
  /api/v1/lists/{listId}/export:
    get:
      tags: [lists]
      summary: Stream list as CSV
      operationId: exportListCsv
      parameters:
        - $ref: "#/components/parameters/listId"
        - name: search
          in: query
          schema: { type: string }
        - name: filename
          in: query
          schema: { type: string }
      responses:
        "200":
          description: text/csv stream
          content:
            text/csv:
              schema: { type: string, format: binary }
        "403":
          description: csvExport entitlement required
  /api/v1/find-runs:
    post:
      tags: [find]
      summary: Create list from plan and start Find
      operationId: startFindRunNewList
      responses:
        "202":
          description: Accepted
  /api/v1/find-runs/{runId}:
    get:
      tags: [find]
      summary: Find run status
      operationId: getFindRun
      parameters:
        - name: runId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: OK
  /api/v1/campaigns:
    get:
      tags: [campaigns]
      summary: List campaigns
      operationId: listCampaigns
      responses:
        "200":
          description: OK
  /api/v1/campaigns/{campaignId}:
    get:
      tags: [campaigns]
      summary: Campaign summary
      operationId: getCampaign
      parameters:
        - $ref: "#/components/parameters/campaignId"
      responses:
        "200":
          description: OK
  /api/v1/campaigns/{campaignId}/enrollments:
    get:
      tags: [enrollments]
      summary: List campaign enrollments
      operationId: listCampaignEnrollments
      parameters:
        - $ref: "#/components/parameters/campaignId"
      responses:
        "200":
          description: OK
    post:
      tags: [enrollments]
      summary: Enroll list row ids (max 100)
      operationId: enrollRows
      parameters:
        - $ref: "#/components/parameters/campaignId"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "201":
          description: Created
  /api/v1/campaigns/{campaignId}/launch:
    post:
      tags: [campaigns]
      summary: Launch draft/paused campaign
      operationId: launchCampaign
      parameters:
        - $ref: "#/components/parameters/campaignId"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "200":
          description: OK
  /api/v1/campaigns/{campaignId}/pause:
    post:
      tags: [campaigns]
      summary: Pause active campaign
      operationId: pauseCampaign
      parameters:
        - $ref: "#/components/parameters/campaignId"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "200":
          description: OK
  /api/v1/inbox/threads:
    get:
      tags: [inbox]
      summary: Campaign inbox thread summary
      operationId: listInboxThreads
      responses:
        "200":
          description: OK
  /api/v1/enrollments/{enrollmentId}:
    get:
      tags: [enrollments]
      summary: Enrollment status
      operationId: getEnrollment
      parameters:
        - $ref: "#/components/parameters/enrollmentId"
      responses:
        "200":
          description: OK
  /api/v1/enrollments/{enrollmentId}/pause:
    post:
      tags: [enrollments]
      summary: Pause enrollment
      operationId: pauseEnrollment
      parameters:
        - $ref: "#/components/parameters/enrollmentId"
      responses:
        "200":
          description: OK
  /api/v1/enrollments/{enrollmentId}/resume:
    post:
      tags: [enrollments]
      summary: Resume enrollment
      operationId: resumeEnrollment
      parameters:
        - $ref: "#/components/parameters/enrollmentId"
      responses:
        "200":
          description: OK
  /api/v1/enrollments/{enrollmentId}/remove:
    post:
      tags: [enrollments]
      summary: Remove enrollment
      operationId: removeEnrollment
      parameters:
        - $ref: "#/components/parameters/enrollmentId"
      responses:
        "200":
          description: OK
  /api/v1/content/posts:
    get:
      tags: [content]
      summary: List content posts
      operationId: listContentPosts
      responses:
        "200":
          description: OK
    post:
      tags: [content]
      summary: Schedule a post
      operationId: scheduleContentPost
      responses:
        "201":
          description: Created
  /api/v1/content/posts/{postId}:
    get:
      tags: [content]
      summary: Get content post
      operationId: getContentPost
      parameters:
        - $ref: "#/components/parameters/postId"
      responses:
        "200":
          description: OK
  /api/v1/content/posts/{postId}/cancel:
    post:
      tags: [content]
      summary: Cancel scheduled post
      operationId: cancelContentPost
      parameters:
        - $ref: "#/components/parameters/postId"
      responses:
        "200":
          description: OK
  /api/v1/accounts:
    get:
      tags: [accounts]
      summary: List channel seats (no credentials)
      operationId: listAccounts
      parameters:
        - name: channel
          in: query
          schema:
            type: string
            enum: [email, linkedin, twitter, phone, sms]
      responses:
        "200":
          description: OK
  /api/v1/accounts/connect:
    post:
      tags: [accounts]
      summary: Start Armada-branded personal account connect (browser URL)
      operationId: startAccountConnect
      responses:
        "200":
          description: OK (connect_id, url, expires_at)
  /api/v1/accounts/connect/{connectId}:
    get:
      tags: [accounts]
      summary: Poll connect session status
      operationId: getAccountConnect
      parameters:
        - name: connectId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: OK
  /api/v1/inventory/catalog:
    get:
      tags: [inventory]
      summary: Inventory catalog (regions, terms, email retail, stock)
      operationId: listInventoryCatalog
      parameters:
        - name: channel
          in: query
          schema:
            type: string
            enum: [email, linkedin]
      responses:
        "200":
          description: OK
  /api/v1/inventory/orders:
    get:
      tags: [inventory]
      summary: List inventory orders
      operationId: listInventoryOrders
      responses:
        "200":
          description: OK
    post:
      tags: [inventory]
      summary: Purchase LinkedIn seats or email infra (card on file or Checkout URL)
      operationId: createInventoryOrder
      responses:
        "200":
          description: OK (order_id, charged, url, seats_created, holds_missing)
  /api/v1/inventory/orders/{orderId}:
    get:
      tags: [inventory]
      summary: Get inventory order status
      operationId: getInventoryOrder
      parameters:
        - name: orderId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: OK
  /api/v1/webhooks:
    get:
      tags: [webhooks]
      summary: List webhook endpoints
      operationId: listWebhooks
      responses:
        "200":
          description: OK
    post:
      tags: [webhooks]
      summary: Register webhook endpoint
      operationId: createWebhook
      responses:
        "201":
          description: Created (includes secret once)
  /api/v1/webhooks/{webhookId}:
    delete:
      tags: [webhooks]
      summary: Disable webhook endpoint
      operationId: deleteWebhook
      parameters:
        - name: webhookId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: OK
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: Workspace API key (`arm_…`)
  parameters:
    listId:
      name: listId
      in: path
      required: true
      schema: { type: string, format: uuid }
    campaignId:
      name: campaignId
      in: path
      required: true
      schema: { type: string, format: uuid }
    enrollmentId:
      name: enrollmentId
      in: path
      required: true
      schema: { type: string, format: uuid }
    postId:
      name: postId
      in: path
      required: true
      schema: { type: string, format: uuid }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: Optional. 1–256 chars [A-Za-z0-9._-]. Successful responses cached 24h per key.
      schema: { type: string, minLength: 1, maxLength: 256, pattern: "^[\\w.-]{1,256}$" }
  responses:
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message, request_id]
          properties:
            code: { type: string }
            message: { type: string }
            request_id: { type: string }
            details: {}
    Me:
      type: object
      required: [workspace_id, organization_id, key, entitlements]
      properties:
        workspace_id: { type: string, format: uuid }
        organization_id: { type: string, format: uuid }
        key:
          type: object
          required: [id, name, scopes]
          properties:
            id: { type: string, format: uuid }
            name: { type: string }
            scopes:
              type: array
              items: { type: string }
        entitlements:
          type: object
          required: [api_access]
          properties:
            api_access: { type: boolean }
            csv_export: { type: boolean }
            plan_id: { type: string, nullable: true }
    ListSummary:
      type: object
      required: [id, name, entity, origin, row_count, created_at, updated_at]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        entity: { type: string, enum: [people, companies] }
        origin: { type: string, enum: [crm, find] }
        row_count: { type: integer, minimum: 0 }
        created_at: { type: string }
        updated_at: { type: string }
    ListDetail:
      allOf:
        - $ref: "#/components/schemas/ListSummary"
        - type: object
          required: [columns]
          properties:
            columns:
              type: array
              items:
                type: object
                required: [id, name, type, position]
                properties:
                  id: { type: string, format: uuid }
                  name: { type: string }
                  type: { type: string }
                  position: { type: integer }
    CreateListBody:
      type: object
      required: [name]
      properties:
        name: { type: string, minLength: 1, maxLength: 120 }
        entity: { type: string, enum: [people, companies], default: people }
        origin: { type: string, enum: [crm, find], default: crm }
