openapi: 3.1.0
info:
  title: Market Fortress Public API
  version: 1.0.0
  description: |
    Programmatic access to a Market Fortress issuer's filings, vault, regulatory
    deadlines, and outbound webhook subscriptions. All endpoints are authenticated
    with a bearer API key scoped to a single issuer. Rate limit is 600 requests
    per minute per key, reported on every authenticated response via the
    X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers.

    Stability classifications are documented per route in openapi/README.md.
  contact:
    name: Market Fortress engineering
    url: https://www.marketfortress.app
  license:
    name: Proprietary
servers:
  - url: https://www.marketfortress.app/api/v1
    description: Production
security:
  - bearerApiKey: []
tags:
  - name: introspection
    description: Token + identity introspection.
  - name: filings
    description: EDGAR filings discovered for the issuer.
  - name: deadlines
    description: Custom + statutory regulatory deadlines.
  - name: vault
    description: Vault snapshots (cap table, debt, financials, registers).
  - name: webhooks
    description: Outbound webhook subscription management.
paths:
  /me:
    get:
      tags: [introspection]
      summary: Resolve the calling API key.
      description: |
        Returns the issuer, scopes, environment, and rate-limit configuration
        for the presented bearer token. Equivalent to GitHub's `/user` or
        Stripe's `/v1/account`. Exempt from the per-key rate limit.
      operationId: getMe
      security:
        - bearerApiKey: [me:read]
      responses:
        '200':
          description: Resolved identity.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    $ref: '#/components/schemas/MeResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /filings:
    get:
      tags: [filings]
      summary: List EDGAR filings for the issuer.
      operationId: listFilings
      security:
        - bearerApiKey: [filings:read]
      parameters:
        - name: form_type
          in: query
          description: Filter by EDGAR form type (e.g. 10-K, 10-Q, 8-K).
          required: false
          schema: { type: string }
        - name: limit
          in: query
          description: Page size, clamped to 1..200. Default 50.
          required: false
          schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
        - name: offset
          in: query
          description: Zero-based offset. Default 0.
          required: false
          schema: { type: integer, minimum: 0, default: 0 }
      responses:
        '200':
          description: Page of filings.
          headers:
            X-RateLimit-Limit: { $ref: '#/components/headers/X-RateLimit-Limit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/X-RateLimit-Remaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/X-RateLimit-Reset' }
            Cache-Control:
              schema: { type: string }
              description: private, s-maxage=300, stale-while-revalidate=600
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/FilingSummary' }
                  meta:
                    $ref: '#/components/schemas/Pagination'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/DatabaseError' }

  /filings/{id}:
    get:
      tags: [filings]
      summary: Get a single filing.
      operationId: getFiling
      security:
        - bearerApiKey: [filings:read]
      parameters:
        - $ref: '#/components/parameters/FilingId'
      responses:
        '200':
          description: Filing detail.
          headers:
            X-RateLimit-Limit: { $ref: '#/components/headers/X-RateLimit-Limit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/X-RateLimit-Remaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/X-RateLimit-Reset' }
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/FilingDetail' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/DatabaseError' }

  /filings/{id}/artifact:
    get:
      tags: [filings]
      summary: Get a short-lived artifact URL for a filing.
      description: |
        Returns the public SEC URL for the filing along with an advisory
        15-minute expiry, suitable for embedding or one-time download flows.
      operationId: getFilingArtifact
      security:
        - bearerApiKey: [filings:read]
      parameters:
        - $ref: '#/components/parameters/FilingId'
      responses:
        '200':
          description: Artifact URL.
          headers:
            X-RateLimit-Limit: { $ref: '#/components/headers/X-RateLimit-Limit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/X-RateLimit-Remaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/X-RateLimit-Reset' }
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/FilingArtifact' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/DatabaseError' }

  /deadlines:
    get:
      tags: [deadlines]
      summary: List the issuer's regulatory deadlines.
      description: |
        Returns custom deadlines stored against the issuer. Statutory deadlines
        derived from filings will fold into the same response shape in a future
        revision without a breaking change.
      operationId: listDeadlines
      security:
        - bearerApiKey: [deadlines:read]
      parameters:
        - name: from
          in: query
          required: false
          schema: { type: string, format: date }
          description: Inclusive lower bound on due_date (YYYY-MM-DD).
        - name: to
          in: query
          required: false
          schema: { type: string, format: date }
          description: Inclusive upper bound on due_date (YYYY-MM-DD).
      responses:
        '200':
          description: List of deadlines.
          headers:
            X-RateLimit-Limit: { $ref: '#/components/headers/X-RateLimit-Limit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/X-RateLimit-Remaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/X-RateLimit-Reset' }
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Deadline' }
                  meta:
                    type: object
                    required: [count]
                    properties:
                      count: { type: integer }
                      from: { type: string, format: date, nullable: true }
                      to: { type: string, format: date, nullable: true }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/DatabaseError' }

  /deadlines/feed.ics:
    get:
      tags: [deadlines]
      summary: RFC 5545 calendar feed of deadlines.
      description: |
        Returns a `text/calendar` feed for Outlook, Google Calendar, and Apple
        Calendar. Subscribers cannot send Authorization headers, so the token
        is passed as a query parameter and must be a calendar-specific token
        of the form `mf_cal_*`. API-key tokens (`mf_live_*`, `mf_test_*`) are
        not accepted. Not subject to the per-API-key rate limit.
      operationId: getDeadlinesIcs
      security: []
      parameters:
        - name: token
          in: query
          required: true
          schema: { type: string, pattern: '^mf_cal_' }
          description: Calendar feed token issued via the developer settings UI.
      responses:
        '200':
          description: VCALENDAR document.
          content:
            text/calendar:
              schema:
                type: string
                example: |
                  BEGIN:VCALENDAR
                  VERSION:2.0
                  PRODID:-//Market Fortress//Deadlines//EN
                  ...
        '401':
          description: Missing or invalid calendar token.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /vault/cap-table:
    get:
      tags: [vault]
      summary: Get the cap table snapshot.
      operationId: getCapTable
      security:
        - bearerApiKey: [vault:read]
      parameters:
        - name: period_end
          in: query
          required: false
          schema: { type: string, format: date }
          description: Snapshot date (YYYY-MM-DD). Defaults to the latest stored.
      responses:
        '200':
          description: Snapshot.
          headers:
            X-RateLimit-Limit: { $ref: '#/components/headers/X-RateLimit-Limit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/X-RateLimit-Remaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/X-RateLimit-Reset' }
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data: { $ref: '#/components/schemas/CapTableSnapshot' }
                  meta:
                    type: object
                    properties: { count: { type: integer } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/DatabaseError' }

  /vault/debt:
    get:
      tags: [vault]
      summary: Get convertible debt and debt schedule.
      operationId: getDebt
      security:
        - bearerApiKey: [vault:read]
      responses:
        '200':
          description: Debt schedule.
          headers:
            X-RateLimit-Limit: { $ref: '#/components/headers/X-RateLimit-Limit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/X-RateLimit-Remaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/X-RateLimit-Reset' }
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: object
                    properties:
                      convertible_debt:
                        type: array
                        items: { type: object, additionalProperties: true }
                      tickers:
                        type: array
                        items: { type: string }
                  meta:
                    type: object
                    properties: { count: { type: integer } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/DatabaseError' }

  /vault/financials:
    get:
      tags: [vault]
      summary: Get the financial snapshot.
      operationId: getFinancials
      security:
        - bearerApiKey: [vault:read]
      parameters:
        - name: period_end
          in: query
          required: false
          schema: { type: string, format: date }
          description: Snapshot date (YYYY-MM-DD). Defaults to the latest stored.
      responses:
        '200':
          description: Financial snapshot.
          headers:
            X-RateLimit-Limit: { $ref: '#/components/headers/X-RateLimit-Limit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/X-RateLimit-Remaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/X-RateLimit-Reset' }
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/FinancialSnapshot' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/DatabaseError' }

  /vault/registers/{register}:
    get:
      tags: [vault]
      summary: Get register entries.
      description: |
        Supported register slugs are `officers`, `directors`, `service_providers`,
        and `share_classes`. Unknown slugs return 404 with the supported list.
      operationId: getRegister
      security:
        - bearerApiKey: [vault:read]
      parameters:
        - name: register
          in: path
          required: true
          schema:
            type: string
            enum: [officers, directors, service_providers, share_classes]
      responses:
        '200':
          description: Register rows.
          headers:
            X-RateLimit-Limit: { $ref: '#/components/headers/X-RateLimit-Limit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/X-RateLimit-Remaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/X-RateLimit-Reset' }
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: array
                    items: { type: object, additionalProperties: true }
                  meta:
                    type: object
                    properties:
                      register: { type: string }
                      count: { type: integer }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404':
          description: Unknown register slug.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Error'
                  - type: object
                    properties:
                      error:
                        type: object
                        properties:
                          supported:
                            type: array
                            items: { type: string }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/DatabaseError' }

  /webhooks:
    get:
      tags: [webhooks]
      summary: List outbound webhook subscriptions.
      operationId: listWebhooks
      security:
        - bearerApiKey: [webhooks:read]
      responses:
        '200':
          description: Subscriptions.
          headers:
            X-RateLimit-Limit: { $ref: '#/components/headers/X-RateLimit-Limit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/X-RateLimit-Remaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/X-RateLimit-Reset' }
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/WebhookSubscription' }
                  meta:
                    type: object
                    properties: { count: { type: integer } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
    post:
      tags: [webhooks]
      summary: Create an outbound webhook subscription.
      description: |
        The signing_secret in the response is returned exactly once. Customers
        must persist it locally; it cannot be retrieved later. URLs are
        validated for SSRF (private IPs, link-local, loopback, RFC1918 blocked)
        and must be HTTPS in production.
      operationId: createWebhook
      security:
        - bearerApiKey: [webhooks:write]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/WebhookCreateRequest' }
      responses:
        '201':
          description: Subscription created.
          headers:
            X-RateLimit-Limit: { $ref: '#/components/headers/X-RateLimit-Limit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/X-RateLimit-Remaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/X-RateLimit-Reset' }
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    allOf:
                      - $ref: '#/components/schemas/WebhookSubscription'
                      - type: object
                        properties:
                          signing_secret: { type: string }
                          signing_secret_notice: { type: string }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /webhooks/{id}:
    get:
      tags: [webhooks]
      summary: Get a single webhook subscription with recent deliveries.
      operationId: getWebhook
      security:
        - bearerApiKey: [webhooks:read]
      parameters:
        - $ref: '#/components/parameters/WebhookId'
      responses:
        '200':
          description: Subscription detail.
          headers:
            X-RateLimit-Limit: { $ref: '#/components/headers/X-RateLimit-Limit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/X-RateLimit-Remaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/X-RateLimit-Reset' }
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    allOf:
                      - $ref: '#/components/schemas/WebhookSubscription'
                      - type: object
                        properties:
                          recent_deliveries:
                            type: array
                            items: { $ref: '#/components/schemas/WebhookDelivery' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
    patch:
      tags: [webhooks]
      summary: Update a webhook subscription.
      operationId: updateWebhook
      security:
        - bearerApiKey: [webhooks:write]
      parameters:
        - $ref: '#/components/parameters/WebhookId'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/WebhookUpdateRequest' }
      responses:
        '200':
          description: Updated subscription.
          headers:
            X-RateLimit-Limit: { $ref: '#/components/headers/X-RateLimit-Limit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/X-RateLimit-Remaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/X-RateLimit-Reset' }
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/WebhookSubscription' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
    delete:
      tags: [webhooks]
      summary: Delete a webhook subscription.
      operationId: deleteWebhook
      security:
        - bearerApiKey: [webhooks:write]
      parameters:
        - $ref: '#/components/parameters/WebhookId'
      responses:
        '204':
          description: Deleted.
          headers:
            X-RateLimit-Limit: { $ref: '#/components/headers/X-RateLimit-Limit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/X-RateLimit-Remaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/X-RateLimit-Reset' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /webhooks/{id}/test:
    post:
      tags: [webhooks]
      summary: Send a synthetic ping to verify the subscription.
      description: |
        Enqueues + immediately attempts a `filing.locked` event with
        `data.test = true`. The response includes the HTTP status returned by
        the customer endpoint so users can validate HMAC verification end-to-end.
      operationId: testWebhook
      security:
        - bearerApiKey: [webhooks:write]
      parameters:
        - $ref: '#/components/parameters/WebhookId'
      responses:
        '200':
          description: Delivery attempt result.
          headers:
            X-RateLimit-Limit: { $ref: '#/components/headers/X-RateLimit-Limit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/X-RateLimit-Remaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/X-RateLimit-Reset' }
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    required: [event_id, delivery_id, status]
                    properties:
                      event_id: { type: string, format: uuid }
                      delivery_id: { type: string, format: uuid }
                      status:
                        type: string
                        enum: [pending, success, failed, dlq]
                      http_status: { type: integer, nullable: true }
                      duration_ms: { type: integer, nullable: true }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

components:
  securitySchemes:
    bearerApiKey:
      type: http
      scheme: bearer
      bearerFormat: mf_live_* | mf_test_*
      description: |
        Bearer token issued from the developer settings UI. Live keys
        (`mf_live_*`) and sandbox keys (`mf_test_*`) share the same shape.
        Sandbox keys bypass the per-key rate limit. Pass via
        `Authorization: Bearer <token>`. Scopes are enforced per route.

  parameters:
    FilingId:
      name: id
      in: path
      required: true
      schema: { type: string, format: uuid }
    WebhookId:
      name: id
      in: path
      required: true
      schema: { type: string, format: uuid }

  headers:
    X-RateLimit-Limit:
      schema: { type: integer }
      description: Maximum requests allowed per window.
    X-RateLimit-Remaining:
      schema: { type: integer }
      description: Requests remaining in the current window after this response.
    X-RateLimit-Reset:
      schema: { type: integer }
      description: Unix timestamp (seconds) at which the current window resets.

  responses:
    Unauthorized:
      description: Missing or invalid bearer token.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Forbidden:
      description: Token does not grant the required scope.
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/Error'
              - type: object
                properties:
                  error:
                    type: object
                    properties:
                      required_scope: { type: string }
                      granted_scopes:
                        type: array
                        items: { type: string }
    NotFound:
      description: Resource not found.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    BadRequest:
      description: Invalid request body or parameters.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    DatabaseError:
      description: Upstream database error.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    RateLimited:
      description: Rate limit exceeded.
      headers:
        Retry-After:
          schema: { type: integer }
          description: Seconds to wait before retrying.
        X-RateLimit-Limit:
          $ref: '#/components/headers/X-RateLimit-Limit'
        X-RateLimit-Remaining:
          schema: { type: integer }
          description: Always 0 when rate-limited.
        X-RateLimit-Reset:
          $ref: '#/components/headers/X-RateLimit-Reset'
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/Error'
              - type: object
                properties:
                  error:
                    type: object
                    properties:
                      retry_after_seconds: { type: integer }

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              description: Machine-readable error code.
              examples:
                - missing_credentials
                - invalid_credentials
                - insufficient_scope
                - rate_limited
                - not_found
                - invalid_body
                - unknown_event_kind
                - create_failed
                - database_error
            message:
              type: string

    Pagination:
      type: object
      required: [total, limit, offset]
      properties:
        total: { type: integer }
        limit: { type: integer }
        offset: { type: integer }

    MeResponse:
      type: object
      required: [key_id, key_prefix, issuer_id, scopes, environment, rate_limit]
      properties:
        key_id: { type: string, format: uuid }
        key_prefix: { type: string, example: mf_live_a3f8 }
        issuer_id: { type: string, format: uuid }
        scopes:
          type: array
          items:
            type: string
            enum:
              - filings:read
              - vault:read
              - deadlines:read
              - webhooks:read
              - webhooks:write
              - me:read
        environment: { type: string, enum: [live, test] }
        rate_limit:
          type: object
          properties:
            limit: { type: integer, example: 600 }
            window_seconds: { type: integer, example: 60 }

    FilingSummary:
      type: object
      required: [id, accession_number, form_type, filed_at, title, status, links]
      properties:
        id: { type: string, format: uuid }
        accession_number: { type: string, example: "0001140361-25-000123" }
        form_type: { type: string, example: 10-K }
        filed_at: { type: string, format: date }
        period_end: { type: string, format: date, nullable: true }
        title: { type: string }
        status: { type: string, enum: [filed] }
        links:
          type: object
          properties:
            artifact: { type: string }
            web: { type: string, format: uri }

    FilingDetail:
      allOf:
        - $ref: '#/components/schemas/FilingSummary'
        - type: object
          properties:
            fetched_at: { type: string, format: date-time }

    FilingArtifact:
      type: object
      required: [artifact_url, expires_at, ttl_seconds]
      properties:
        artifact_url: { type: string, format: uri }
        expires_at: { type: string, format: date-time }
        ttl_seconds: { type: integer, example: 900 }

    Deadline:
      type: object
      required: [id, title, due_date, authority, status]
      properties:
        id: { type: string, format: uuid }
        title: { type: string }
        description: { type: string, nullable: true }
        due_date: { type: string, format: date }
        authority:
          type: string
          description: Issuing authority (SEC, IRS, OTC, FINRA, NYSE, NASDAQ, etc).
        event_type: { type: string, nullable: true }
        category: { type: string, nullable: true }
        priority: { type: string, nullable: true }
        status: { type: string }
        is_hard_deadline: { type: boolean }

    CapTableSnapshot:
      type: object
      properties:
        period_end: { type: string, format: date, nullable: true }
        classes:
          type: array
          items:
            type: object
            additionalProperties: true

    FinancialSnapshot:
      type: object
      properties:
        id: { type: string, format: uuid }
        period_end: { type: string, format: date }
        period_type: { type: string }
        balance_sheet:
          type: object
          additionalProperties: true
        income_statement:
          type: object
          additionalProperties: true
        cash_flow:
          type: object
          additionalProperties: true
        source_filing_id: { type: string, format: uuid, nullable: true }
        parse_confidence: { type: number, nullable: true }
        updated_at: { type: string, format: date-time }

    WebhookEventKind:
      type: string
      enum:
        - filing.locked
        - filing.accepted
        - filing.rejected
        - deadline.t-minus-7
        - deadline.t-minus-3
        - deadline.t-minus-1
        - deadline.passed
        - audit.anomaly.detected
        - materiality.threshold.crossed
        - cap_table.changed
        - vote.cast
        - concierge.draft.review_requested
        - counsel.review.completed
        - section16.late_filing.detected
        - period_close.workspace.advanced
        - restatement.workflow.advanced
        - despac.transaction.advanced

    WebhookSubscription:
      type: object
      required: [id, issuer_id, url, events, is_active, failure_count, created_at]
      properties:
        id: { type: string, format: uuid }
        issuer_id: { type: string, format: uuid }
        url: { type: string, format: uri }
        events:
          type: array
          items:
            oneOf:
              - $ref: '#/components/schemas/WebhookEventKind'
              - type: string
                enum: ['*']
        is_active: { type: boolean }
        failure_count: { type: integer }
        last_success_at: { type: string, format: date-time, nullable: true }
        last_failure_at: { type: string, format: date-time, nullable: true }
        last_failure_status: { type: integer, nullable: true }
        description: { type: string, nullable: true }
        created_by: { type: string }
        created_at: { type: string, format: date-time }
        disabled_at: { type: string, format: date-time, nullable: true }

    WebhookCreateRequest:
      type: object
      required: [url, events]
      properties:
        url:
          type: string
          format: uri
          description: HTTPS URL (HTTP allowed in non-production only). Private/loopback/link-local addresses are rejected.
        events:
          type: array
          minItems: 1
          items:
            oneOf:
              - $ref: '#/components/schemas/WebhookEventKind'
              - type: string
                enum: ['*']
        description: { type: string }

    WebhookUpdateRequest:
      type: object
      properties:
        url: { type: string, format: uri }
        events:
          type: array
          items:
            oneOf:
              - $ref: '#/components/schemas/WebhookEventKind'
              - type: string
                enum: ['*']
        description: { type: string, nullable: true }
        is_active: { type: boolean }

    WebhookDelivery:
      type: object
      required: [id, subscription_id, event_id, event_kind, attempt, status, created_at]
      properties:
        id: { type: string, format: uuid }
        subscription_id: { type: string, format: uuid }
        event_id: { type: string, format: uuid }
        event_kind: { $ref: '#/components/schemas/WebhookEventKind' }
        payload:
          type: object
          additionalProperties: true
        attempt: { type: integer }
        status:
          type: string
          enum: [pending, success, failed, dlq]
        http_status: { type: integer, nullable: true }
        response_body: { type: string, nullable: true }
        duration_ms: { type: integer, nullable: true }
        next_retry_at: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time }

    WebhookPayload:
      type: object
      required: [id, kind, created_at, issuer_id, data]
      description: |
        Payload posted to subscriber URLs. Signed with HMAC-SHA256 over
        `<unix_timestamp>.<raw_body>` using the subscription's signing_secret.
        Signature is delivered in the `Mf-Webhook-Signature` header as
        `t=<unix>,v1=<hex>`. Verification window is 5 minutes.
      properties:
        id:
          type: string
          format: uuid
          description: Event id. Also serves as the idempotency key for receivers.
        kind: { $ref: '#/components/schemas/WebhookEventKind' }
        created_at: { type: string, format: date-time }
        issuer_id: { type: string, format: uuid }
        data:
          type: object
          additionalProperties: true
