openapi: 3.1.0
info:
  title: Leadex API
  version: "1.0.0"
  description: |
    Chat-native lead research, programmatically.
    See the full docs at https://docs.getleadex.com/.
  contact:
    name: Leadex support
    email: support@getleadex.com
    url: https://docs.getleadex.com/
  license:
    name: Proprietary
servers:
  - url: https://api.getleadex.com/v1
    description: Production

security:
  - bearerAuth: []

tags:
  - name: Threads
  - name: Messages
  - name: Plans
  - name: Jobs
  - name: Streaming
  - name: Artifacts
  - name: Search
  - name: Integrations
  - name: Webhooks
  - name: Usage
  - name: Account

paths:
  /threads:
    get:
      tags: [Threads]
      summary: List threads
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
        - name: q
          in: query
          schema: { type: string }
        - name: has_job_status
          in: query
          schema: { type: string }
      responses:
        "200": { $ref: "#/components/responses/ThreadList" }
    post:
      tags: [Threads]
      summary: Create a thread
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                title:    { type: string, maxLength: 200 }
                metadata: { $ref: "#/components/schemas/Metadata" }
      responses:
        "201": { $ref: "#/components/responses/Thread" }

  /threads/{thread_id}:
    parameters: [ { $ref: "#/components/parameters/ThreadId" } ]
    get:
      tags: [Threads]
      summary: Retrieve a thread
      responses: { "200": { $ref: "#/components/responses/Thread" } }
    patch:
      tags: [Threads]
      summary: Update a thread
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                title:    { type: string, maxLength: 200 }
                metadata: { $ref: "#/components/schemas/Metadata" }
      responses: { "200": { $ref: "#/components/responses/Thread" } }
    delete:
      tags: [Threads]
      summary: Delete a thread
      parameters:
        - name: force
          in: query
          schema: { type: boolean, default: false }
      responses:
        "204": { description: Deleted }
        "409": { $ref: "#/components/responses/Error" }

  /threads/{thread_id}/messages:
    parameters: [ { $ref: "#/components/parameters/ThreadId" } ]
    get:
      tags: [Messages]
      summary: List messages
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses: { "200": { $ref: "#/components/responses/MessageList" } }
    post:
      tags: [Messages]
      summary: Send a user message (triggers planner)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [content]
              properties:
                content:      { type: string, minLength: 1, maxLength: 8000 }
                attachments:
                  type: array
                  maxItems: 5
                  items:
                    type: object
                    properties:
                      kind: { type: string, enum: [csv, url_list] }
                      url:  { type: string, format: uri }
                hints:
                  type: object
                  properties:
                    max_rows:          { type: integer, minimum: 1, maximum: 10000 }
                    preferred_sources: { type: array, items: { type: string } }
                auto_approve: { type: boolean, default: false }
      responses:
        "201":
          description: Message created; plan and pending job attached.
          content:
            application/json:
              schema:
                type: object
                properties:
                  message: { $ref: "#/components/schemas/Message" }
                  plan:    { $ref: "#/components/schemas/Plan" }
                  job:     { $ref: "#/components/schemas/Job" }

  /plans/{plan_id}:
    parameters: [ { $ref: "#/components/parameters/PlanId" } ]
    get:
      tags: [Plans]
      summary: Retrieve a plan
      responses: { "200": { $ref: "#/components/responses/Plan" } }

  /plans/{plan_id}/replan:
    parameters: [ { $ref: "#/components/parameters/PlanId" } ]
    post:
      tags: [Plans]
      summary: Regenerate a plan with feedback
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [feedback]
              properties:
                feedback: { type: string, minLength: 1, maxLength: 2000 }
      responses:
        "201":
          description: New plan + new pending job; previous pending job is cancelled.
          content:
            application/json:
              schema:
                type: object
                properties:
                  plan: { $ref: "#/components/schemas/Plan" }
                  job:  { $ref: "#/components/schemas/Job" }

  /jobs:
    get:
      tags: [Jobs]
      summary: List jobs
      parameters:
        - name: status
          in: query
          schema: { type: string }
        - name: thread_id
          in: query
          schema: { type: string }
        - name: created_after
          in: query
          schema: { type: string, format: date-time }
        - name: created_before
          in: query
          schema: { type: string, format: date-time }
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses: { "200": { $ref: "#/components/responses/JobList" } }

  /jobs/{job_id}:
    parameters: [ { $ref: "#/components/parameters/JobId" } ]
    get:
      tags: [Jobs]
      summary: Retrieve a job
      responses: { "200": { $ref: "#/components/responses/Job" } }

  /jobs/{job_id}/approve:
    parameters: [ { $ref: "#/components/parameters/JobId" } ]
    post:
      tags: [Jobs]
      summary: Approve and start a pending job
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                webhook_url: { type: string, format: uri }
                max_credits: { type: integer, minimum: 1 }
      responses:
        "200": { $ref: "#/components/responses/Job" }
        "409": { $ref: "#/components/responses/Error" }

  /jobs/{job_id}/stop:
    parameters: [ { $ref: "#/components/parameters/JobId" } ]
    post:
      tags: [Jobs]
      summary: Stop a running job
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                reason: { type: string, maxLength: 500 }
      responses: { "200": { $ref: "#/components/responses/Job" } }

  /jobs/{job_id}/logs:
    parameters: [ { $ref: "#/components/parameters/JobId" } ]
    get:
      tags: [Jobs]
      summary: Fetch stored job logs
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Log entries for a job.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/LogEntry" }
                  has_more:    { type: boolean }
                  next_cursor: { type: string, nullable: true }

  /jobs/{job_id}/stream:
    parameters: [ { $ref: "#/components/parameters/JobId" } ]
    get:
      tags: [Streaming]
      summary: Open an SSE event stream
      description: |
        Server-Sent Events. Emits `log`, `step`, `result`, `artifact`, `error`,
        and `done` events. Reconnect with `Last-Event-ID` to resume.
      responses:
        "200":
          description: text/event-stream
          content:
            text/event-stream: {}

  /jobs/{job_id}/leads:
    parameters: [ { $ref: "#/components/parameters/JobId" } ]
    get:
      tags: [Artifacts]
      summary: List leads produced by a job
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
        - name: min_confidence
          in: query
          schema: { type: number, minimum: 0, maximum: 1 }
        - name: has_field
          in: query
          schema: { type: array, items: { type: string } }
          style: form
          explode: true
      responses:
        "200":
          description: Paginated leads.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Lead" }
                  has_more:    { type: boolean }
                  next_cursor: { type: string, nullable: true }

  /jobs/{job_id}/summary:
    parameters: [ { $ref: "#/components/parameters/JobId" } ]
    get:
      tags: [Artifacts]
      summary: Retrieve the LLM summary
      responses:
        "200":
          description: Summary text and metadata.
          content:
            application/json:
              schema:
                type: object
                properties:
                  summary: { type: string }
                  caveats: { type: array, items: { type: string } }
                  rows:    { type: integer }

  /jobs/{job_id}/push:
    parameters: [ { $ref: "#/components/parameters/JobId" } ]
    post:
      tags: [Integrations]
      summary: Push leads to a connected CRM
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [provider]
              properties:
                provider: { type: string, enum: [hubspot, salesforce, pipedrive, attio, close, zoho, copper, google_sheets] }
                mode:     { type: string, enum: [contacts, companies, both], default: contacts }
                list_id:  { type: string }
                mapping:  { type: object, additionalProperties: { type: string } }
                dry_run:  { type: boolean, default: false }
      responses:
        "200":
          description: Push result.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PushResult" }

  /artifacts/{artifact_id}:
    parameters:
      - name: artifact_id
        in: path
        required: true
        schema: { type: string }
    get:
      tags: [Artifacts]
      summary: Retrieve (and sign) an artifact download URL
      parameters:
        - name: format
          in: query
          schema: { type: string, enum: [csv, json, xlsx], default: csv }
      responses:
        "200": { $ref: "#/components/responses/Artifact" }

  /search:
    post:
      tags: [Search]
      summary: One-shot audience search (prompt -> leads)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [prompt]
              properties:
                prompt:      { type: string, minLength: 1, maxLength: 8000 }
                max_rows:    { type: integer, minimum: 1, maximum: 2000, default: 200 }
                max_credits: { type: integer, minimum: 1 }
                wait:        { type: string, enum: [sync, async], default: sync }
                format:      { type: string, enum: [json, csv], default: json }
      responses:
        "200":
          description: Results inline (`wait=sync`) or job pointer (`wait=async`).
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/SearchSyncResult"
                  - $ref: "#/components/schemas/SearchAsyncResult"

  /integrations:
    get:
      tags: [Integrations]
      summary: List connected integrations
      responses:
        "200":
          description: Connected integrations.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Integration" }

  /integrations/{provider}/connect:
    parameters:
      - name: provider
        in: path
        required: true
        schema: { type: string }
    post:
      tags: [Integrations]
      summary: Start an OAuth connection flow
      responses:
        "200":
          description: Redirect URL.
          content:
            application/json:
              schema:
                type: object
                properties:
                  redirect_url: { type: string, format: uri }

  /integrations/{provider}:
    parameters:
      - name: provider
        in: path
        required: true
        schema: { type: string }
    delete:
      tags: [Integrations]
      summary: Disconnect an integration
      responses:
        "204": { description: Disconnected }

  /webhooks:
    get:
      tags: [Webhooks]
      summary: List webhook endpoints
      responses:
        "200":
          description: Configured endpoints.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Webhook" }
    post:
      tags: [Webhooks]
      summary: Register a webhook endpoint
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url:         { type: string, format: uri }
                events:      { type: array, items: { type: string }, default: ["*"] }
                description: { type: string, maxLength: 200 }
                enabled:     { type: boolean, default: true }
      responses:
        "201":
          description: Newly-registered webhook, including the signing secret (returned once).
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Webhook"
                  - type: object
                    properties:
                      signing_secret: { type: string }

  /webhooks/{webhook_id}:
    parameters:
      - name: webhook_id
        in: path
        required: true
        schema: { type: string }
    patch:
      tags: [Webhooks]
      summary: Update a webhook endpoint
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                url:         { type: string, format: uri }
                events:      { type: array, items: { type: string } }
                description: { type: string, maxLength: 200 }
                enabled:     { type: boolean }
      responses:
        "200":
          description: Updated webhook.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Webhook" }
    delete:
      tags: [Webhooks]
      summary: Delete a webhook endpoint
      responses:
        "204": { description: Deleted }

  /webhooks/{webhook_id}/rotate:
    parameters:
      - name: webhook_id
        in: path
        required: true
        schema: { type: string }
    post:
      tags: [Webhooks]
      summary: Rotate the webhook signing secret
      responses:
        "200":
          description: New signing secret.
          content:
            application/json:
              schema:
                type: object
                properties:
                  signing_secret: { type: string }

  /webhooks/{webhook_id}/test:
    parameters:
      - name: webhook_id
        in: path
        required: true
        schema: { type: string }
    post:
      tags: [Webhooks]
      summary: Send a test delivery
      responses:
        "202": { description: Accepted for delivery }

  /usage:
    get:
      tags: [Usage]
      summary: Usage summary for the current period
      parameters:
        - name: from
          in: query
          schema: { type: string, format: date }
        - name: to
          in: query
          schema: { type: string, format: date }
        - name: group_by
          in: query
          schema: { type: string, enum: [day, thread, user] }
      responses:
        "200":
          description: Usage summary.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Usage" }

  /usage/budget:
    get:
      tags: [Usage]
      summary: Retrieve the organization budget ceiling
      responses:
        "200":
          description: Current budget state.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Budget" }
    put:
      tags: [Usage]
      summary: Set the organization budget ceiling
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                monthly_credit_ceiling: { type: integer, minimum: 0 }
                hard_stop:              { type: boolean, default: true }
      responses:
        "200":
          description: Updated budget.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Budget" }

  /me:
    get:
      tags: [Account]
      summary: Current principal (user + org)
      responses:
        "200":
          description: Authenticated principal.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Principal" }

  /organization:
    get:
      tags: [Account]
      summary: Key's organization
      responses:
        "200":
          description: Organization details.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Organization" }

  /api-keys:
    get:
      tags: [Account]
      summary: List API keys
      responses:
        "200":
          description: Key metadata (no secrets).
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ApiKey" }
    post:
      tags: [Account]
      summary: Mint a new API key (secret returned once)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:       { type: string, maxLength: 100 }
                scopes:     { type: array, items: { type: string } }
                expires_at: { type: string, format: date-time, nullable: true }
      responses:
        "201":
          description: Newly-created key; includes the secret value exactly once.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiKey"
                  - type: object
                    properties:
                      secret: { type: string }

  /api-keys/{key_id}:
    parameters:
      - name: key_id
        in: path
        required: true
        schema: { type: string }
    delete:
      tags: [Account]
      summary: Revoke an API key
      responses:
        "204": { description: Revoked }

  /members:
    get:
      tags: [Account]
      summary: List organization members
      responses:
        "200":
          description: Org members (admin scope required).
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Member" }
    post:
      tags: [Account]
      summary: Invite a member by email
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email: { type: string, format: email }
                role:  { type: string, enum: [admin, member, viewer], default: member }
      responses:
        "201":
          description: Invite sent.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Member" }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer

  parameters:
    ThreadId:
      name: thread_id
      in: path
      required: true
      schema: { type: string }
    PlanId:
      name: plan_id
      in: path
      required: true
      schema: { type: string }
    JobId:
      name: job_id
      in: path
      required: true
      schema: { type: string }
    Limit:
      name: limit
      in: query
      schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
    Cursor:
      name: cursor
      in: query
      schema: { type: string }

  responses:
    Thread:
      description: A single thread.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Thread" }
    ThreadList:
      description: Paginated list of threads.
      content:
        application/json:
          schema:
            type: object
            properties:
              data:        { type: array, items: { $ref: "#/components/schemas/Thread" } }
              has_more:    { type: boolean }
              next_cursor: { type: string, nullable: true }
    MessageList:
      description: Paginated list of messages.
      content:
        application/json:
          schema:
            type: object
            properties:
              data:        { type: array, items: { $ref: "#/components/schemas/Message" } }
              has_more:    { type: boolean }
              next_cursor: { type: string, nullable: true }
    Plan:
      description: A single plan.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Plan" }
    Job:
      description: A single job.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Job" }
    JobList:
      description: Paginated list of jobs.
      content:
        application/json:
          schema:
            type: object
            properties:
              data:        { type: array, items: { $ref: "#/components/schemas/Job" } }
              has_more:    { type: boolean }
              next_cursor: { type: string, nullable: true }
    Artifact:
      description: Artifact record with a signed download URL.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Artifact" }
    Error:
      description: Error envelope.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }

  schemas:
    Metadata:
      type: object
      additionalProperties: { type: string, maxLength: 500 }
      maxProperties: 50

    Thread:
      type: object
      properties:
        id:            { type: string }
        object:        { type: string, enum: [thread] }
        title:         { type: string, maxLength: 200 }
        created_at:    { type: string, format: date-time }
        updated_at:    { type: string, format: date-time }
        message_count: { type: integer }
        last_job_id:   { type: string, nullable: true }
        metadata:      { $ref: "#/components/schemas/Metadata" }

    Message:
      type: object
      properties:
        id:         { type: string }
        object:     { type: string, enum: [message] }
        thread_id:  { type: string }
        role:       { type: string, enum: [user, assistant, system] }
        kind:       { type: string, enum: [text, plan, result, error] }
        content:    { type: string }
        plan_id:    { type: string, nullable: true }
        job_id:     { type: string, nullable: true }
        created_at: { type: string, format: date-time }

    Plan:
      type: object
      properties:
        id:                { type: string }
        object:            { type: string, enum: [plan] }
        job_id:            { type: string }
        title:             { type: string }
        steps:
          type: array
          maxItems: 12
          items: { $ref: "#/components/schemas/Step" }
        credits_estimate:  { type: integer }
        created_at:        { type: string, format: date-time }

    Step:
      type: object
      properties:
        id:                { type: string }
        title:             { type: string }
        intent:            { type: string, enum: [discover, enrich, verify, dedupe] }
        task:              { type: string }
        expected_columns:  { type: array, items: { type: string } }

    Job:
      type: object
      properties:
        id:           { type: string }
        object:       { type: string, enum: [job] }
        thread_id:    { type: string }
        plan_id:      { type: string }
        status:       { type: string, enum: [pending, running, succeeded, failed, stopped] }
        progress:
          type: object
          properties:
            step:    { type: integer }
            total:   { type: integer }
            percent: { type: number }
        started_at:   { type: string, format: date-time, nullable: true }
        finished_at:  { type: string, format: date-time, nullable: true }
        credits_used: { type: integer }
        artifact:
          type: object
          nullable: true
          properties:
            id:    { type: string }
            rows:  { type: integer }
            bytes: { type: integer }
        summary:      { type: string, nullable: true }
        error:
          type: object
          nullable: true
          properties:
            code:    { type: string }
            message: { type: string }
        metadata:     { $ref: "#/components/schemas/Metadata" }

    Artifact:
      type: object
      properties:
        id:           { type: string }
        object:       { type: string, enum: [artifact] }
        job_id:       { type: string }
        format:       { type: string, enum: [csv, json, xlsx] }
        rows:         { type: integer }
        bytes:        { type: integer }
        columns:      { type: array, items: { type: string } }
        download_url: { type: string, format: uri }
        expires_at:   { type: string, format: date-time }
        created_at:   { type: string, format: date-time }

    Lead:
      type: object
      properties:
        id:     { type: string }
        object: { type: string, enum: [lead] }
        job_id: { type: string }
        company:
          type: object
          properties:
            name:     { type: string }
            domain:   { type: string }
            linkedin: { type: string, nullable: true }
            hq_city:  { type: string, nullable: true }
        person:
          type: object
          nullable: true
          properties:
            full_name: { type: string }
            title:     { type: string }
            linkedin:  { type: string, nullable: true }
            email:     { type: string, nullable: true }
        fields:
          type: object
          additionalProperties: true
        sources:
          type: array
          items:
            type: object
            properties:
              kind:         { type: string }
              url:          { type: string, format: uri }
              retrieved_at: { type: string, format: date-time }
        confidence: { type: number, minimum: 0, maximum: 1 }

    LogEntry:
      type: object
      properties:
        id:      { type: string }
        ts:      { type: string, format: date-time }
        kind:    { type: string, enum: [step, log, result, artifact, error, done] }
        message: { type: string }
        step_id: { type: string, nullable: true }

    SearchSyncResult:
      type: object
      properties:
        job_id:   { type: string }
        leads:
          type: array
          items: { $ref: "#/components/schemas/Lead" }
        csv:      { type: string, nullable: true }
        artifact: { $ref: "#/components/schemas/Artifact" }
        summary:  { type: string }

    SearchAsyncResult:
      type: object
      properties:
        job_id: { type: string }
        status: { type: string, enum: [pending, running] }

    PushResult:
      type: object
      properties:
        push_id:  { type: string }
        provider: { type: string }
        created:  { type: integer }
        updated:  { type: integer }
        skipped:  { type: integer }
        errors:
          type: array
          items:
            type: object
            properties:
              lead_id: { type: string }
              message: { type: string }
        list_id:  { type: string, nullable: true }
        list_url: { type: string, format: uri, nullable: true }

    Integration:
      type: object
      properties:
        provider:     { type: string }
        status:       { type: string, enum: [connected, disconnected, error] }
        connected_by: { type: string }
        connected_at: { type: string, format: date-time }
        scopes:       { type: array, items: { type: string } }

    Webhook:
      type: object
      properties:
        id:          { type: string }
        object:      { type: string, enum: [webhook] }
        url:         { type: string, format: uri }
        events:      { type: array, items: { type: string } }
        description: { type: string, nullable: true }
        enabled:     { type: boolean }
        created_at:  { type: string, format: date-time }

    Usage:
      type: object
      properties:
        period:
          type: object
          properties:
            from: { type: string, format: date }
            to:   { type: string, format: date }
        plan: { type: string, enum: [starter, growth, scale, enterprise] }
        credits:
          type: object
          properties:
            included:  { type: integer }
            used:      { type: integer }
            remaining: { type: integer }
            rollover:  { type: integer }
        jobs:
          type: object
          properties:
            succeeded: { type: integer }
            failed:    { type: integer }
            stopped:   { type: integer }
        by_day:
          type: array
          items:
            type: object
            properties:
              date:    { type: string, format: date }
              credits: { type: integer }
              jobs:    { type: integer }

    Budget:
      type: object
      properties:
        monthly_credit_ceiling: { type: integer, nullable: true }
        hard_stop:              { type: boolean }
        used_this_period:       { type: integer }
        resets_at:              { type: string, format: date-time }

    Principal:
      type: object
      properties:
        user:
          type: object
          properties:
            id:    { type: string }
            email: { type: string, format: email }
            name:  { type: string }
        organization: { $ref: "#/components/schemas/Organization" }
        scopes:       { type: array, items: { type: string } }

    Organization:
      type: object
      properties:
        id:         { type: string }
        name:       { type: string }
        plan:       { type: string }
        created_at: { type: string, format: date-time }

    ApiKey:
      type: object
      properties:
        id:         { type: string }
        name:       { type: string }
        prefix:     { type: string }
        last4:      { type: string }
        scopes:     { type: array, items: { type: string } }
        created_at: { type: string, format: date-time }
        expires_at: { type: string, format: date-time, nullable: true }
        last_used_at: { type: string, format: date-time, nullable: true }

    Member:
      type: object
      properties:
        id:         { type: string }
        email:      { type: string, format: email }
        name:       { type: string, nullable: true }
        role:       { type: string, enum: [admin, member, viewer] }
        invited_at: { type: string, format: date-time }
        joined_at:  { type: string, format: date-time, nullable: true }

    ErrorEnvelope:
      type: object
      properties:
        error:
          type: object
          properties:
            code:       { type: string }
            message:    { type: string }
            param:      { type: string, nullable: true }
            request_id: { type: string }
