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

# Analyze up to 50 emails

> Synchronously score 1–50 emails. Each item consumes one request-unit.
Maximum 50 items per request (`LIMIT_EXCEEDED` if exceeded). Partial
failure is per-item via `results[].error`. Required scope: `analyze`.




## OpenAPI

````yaml /docs/api/openapi.yaml post /v1/analyze-email/batch
openapi: 3.1.0
info:
  title: Signup Risk API
  version: 1.8.0-direct012
  description: >
    # Signup Risk API


    > **Stop fake signups before they hit your database.**


    Real-time signup risk scoring for signup and lead-quality flows. Returns a

    deterministic 0–100 risk score with an `allow` / `review` / `block`

    recommendation, backed by disposable-email detection, free/business email

    classification, name/email alignment, IP reputation, DNS infrastructure

    reputation, role-address and domain-typo detection, synthetic-identity

    detection, industry-specific scoring profiles, asynchronous bulk file

    processing, and signed outbound webhook delivery.


    This is **not** a basic email-syntax API. It is a check-driven decision

    system purpose-built for signup trust and abuse prevention.


    ## Authentication


    The first-party API uses **Bearer API-key** authentication:


    ```

    Authorization: Bearer eisk_live_<public-id>_<secret>

    ```


    API keys are issued from the dashboard at `https://app.signuprisk.com`

    and have a reveal-once secret. Scopes (`analyze`, `bulk`, `webhooks`) permit

    a route; the account **plan** permits premium functionality. Scope and plan

    are independent — both must pass.


    Dashboard, billing, and playground routes use **Clerk session** Bearer JWTs

    (human dashboard sessions), never API keys. The public checker is anonymous.


    ## Quickstart


    ```bash

    curl https://api.signuprisk.com/v1/analyze-email \
      -H "Authorization: Bearer eisk_live_example_public_id_example_secret" \
      -H "Content-Type: application/json" \
      -d '{"email":"person@example.com"}'
    ```


    See the [Quickstart](https://docs.signuprisk.com/quickstart) for the full
    first-party guide.
  contact:
    name: Signup Risk API Support
    url: https://app.signuprisk.com/support
  license:
    name: Proprietary
    url: https://app.signuprisk.com/terms
servers:
  - url: https://api.signuprisk.com
    description: Production
  - url: http://localhost:8080
    description: Local development (not a production server)
security: []
tags:
  - name: Analysis
    description: Synchronous single and batch email risk analysis.
  - name: Bulk
    description: Asynchronous bulk file processing.
  - name: Customer Webhooks
    description: >
      Outbound (customer-facing) webhook configuration and delivery.


      Delivery wire format (each delivery is an HTTPS POST):
        - Algorithm: HMAC-SHA256 with the per-config reveal-once signing secret.
        - X-SignupRisk-Event: the event type (e.g. `signup.high_risk`).
        - X-SignupRisk-Delivery: the delivery id (STABLE across retries; receivers
          dedupe on it).
        - X-SignupRisk-Timestamp: unix seconds (receiver should reject deliveries
          older than ~5 minutes as replay protection).
        - X-SignupRisk-Signature: `t=<timestamp>,v1=<hex(hmac_sha256(secret, "<timestamp>.<raw_body_bytes>"))>`.
        - Content-Type: application/json. User-Agent: signup-risk-webhooks/1.0.
      The signature is computed over the literal `"<timestamp>.<raw_body>"`
      using

      the RAW request body bytes (do NOT re-serialize JSON before verifying).

      Receiver response semantics: 2xx = success (terminal); 3xx = followed up
      to

      10 hops (each re-SSRF-checked; exhausted = retryable); 4xx = terminal

      failure (not retried); 5xx/network/timeout = retried with exponential

      backoff (~1m, 5m, 25m, 2h, 6h) up to max_attempts (default 5), then
      `dead`.

      Direct-014B guarantee: at most one active sender owns a delivery claim.
  - name: API Keys
    description: Dashboard management of first-party API keys.
  - name: Dashboard
    description: Clerk-session-protected backend-for-frontend dashboard routes.
  - name: Admin Console
    description: |
      Internal operator surface for support (base `/v1/admin`); not a customer
      API. Every route requires an authenticated, active Clerk session (same
      scheme as dashboard routes) carrying the custom `admin_role` session-JWT
      claim (`admin` or `support`). Both roles may read; writes additionally
      require `admin`. A missing or insufficient role is `403
      ADMIN_ROLE_REQUIRED`. Routes are registered only when the server feature
      flag `ADMIN_CONSOLE_ENABLED=true` is set. PII-free by design: user views
      carry no email fields; lookup by email is hash-matched server-side and
      responses contain only resolved ids/status.
  - name: Usage
    description: Account usage, quota windows, and history.
  - name: Billing
    description: Stripe self-service billing (Checkout, Portal, state).
  - name: Playground
    description: Authenticated dashboard playground that consumes real quota.
  - name: Public Checker
    description: Anonymous, limited, separately rate-limited public checker.
  - name: Public Playground
    description: >-
      Anonymous pre-signup playground returning the full envelope under a strict
      per-IP daily limit.
  - name: Operations
    description: Health and liveness probes.
  - name: Provider Webhooks
    description: Inbound provider-owned callback routes (Clerk, Stripe).
externalDocs:
  description: DIRECT-012 package documentation and references
  url: >-
    https://github.com/saviodo/email-intelligence-api/blob/main/docs/features/DIRECT-012-public-api-documentation-openapi-migration.md
paths:
  /v1/analyze-email/batch:
    post:
      tags:
        - Analysis
      summary: Analyze up to 50 emails
      description: |
        Synchronously score 1–50 emails. Each item consumes one request-unit.
        Maximum 50 items per request (`LIMIT_EXCEEDED` if exceeded). Partial
        failure is per-item via `results[].error`. Required scope: `analyze`.
      parameters:
        - $ref: '#/components/parameters/RequestIdHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BatchAnalysisRequest'
            examples:
              simple:
                value:
                  emails:
                    - person@example.com
                    - abuse@example.org
      responses:
        '200':
          description: Batch result (per-item success/failure).
          headers:
            X-Request-ID:
              schema:
                type: string
            X-RateLimit-Limit:
              schema:
                type: integer
            X-RateLimit-Remaining:
              schema:
                type: integer
            X-Quota-Limit:
              schema:
                type: integer
            X-Quota-Used:
              schema:
                type: integer
            X-Quota-Remaining:
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchAnalysisResponse'
              examples:
                reserved_domains:
                  summary: Two documentation-reserved addresses
                  description: >
                    Both example addresses use IANA-reserved special-use
                    domains, so each item deterministically returns block (risk
                    score 100). checks and explanation are null because the
                    request did not enable them via options.
                  value:
                    count: 2
                    success_count: 2
                    failure_count: 0
                    results:
                      - index: 0
                        success: true
                        email:
                          input: person@example.com
                          normalized: person@example.com
                          normalized_preserving_case: person@example.com
                          canonical: person@example.com
                          is_well_formed: true
                          format_issues: []
                          format_explanation: >-
                            The email address is structurally well formed. This
                            does not mean it is safe, trusted, deliverable, or
                            non-disposable.
                          is_internationalized: false
                        decision:
                          action: block
                          risk_score: 100
                          risk_band: very_high
                          summary: >-
                            Very high-risk signup. Primary concern:
                            IANA-reserved special-use domain that can never
                            receive email.
                          assessment:
                            confidence: medium
                            evidence_strength: strong
                            evidence_coverage: broad
                            uncertainty: low
                            limitations: []
                        checks: null
                        explanation: null
                        error: null
                      - index: 1
                        success: true
                        email:
                          input: abuse@example.org
                          normalized: abuse@example.org
                          normalized_preserving_case: abuse@example.org
                          canonical: abuse@example.org
                          is_well_formed: true
                          format_issues: []
                          format_explanation: >-
                            The email address is structurally well formed. This
                            does not mean it is safe, trusted, deliverable, or
                            non-disposable.
                          is_internationalized: false
                        decision:
                          action: block
                          risk_score: 100
                          risk_band: very_high
                          summary: >-
                            Very high-risk signup. Primary concern:
                            IANA-reserved special-use domain that can never
                            receive email.
                          assessment:
                            confidence: high
                            evidence_strength: strong
                            evidence_coverage: broad
                            uncertainty: low
                            limitations: []
                        checks: null
                        explanation: null
                        error: null
                    metadata:
                      warnings: []
                      usage:
                        requests:
                          used: 2
                          limit: 10000
                          unlimited: false
                          remaining: 9998
                        explanations:
                          used: 0
                          limit: 500
                          unlimited: false
                          remaining: 500
                      plan:
                        current_plan: Developer
                        billing_period:
                          starts: '2026-09-16T00:00:00Z'
                          ends: '2026-10-16T00:00:00Z'
                        reset_at: '2026-10-16T00:00:00Z'
                      assessment_version: 2026.08.28
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
      security:
        - ApiKeyBearer: []
components:
  parameters:
    RequestIdHeader:
      in: header
      name: X-Request-ID
      required: false
      schema:
        type: string
        maxLength: 128
      description: |
        Optional client-supplied request id. The server always echoes an
        `X-Request-ID` response header; malformed/too-long values are replaced.
        Safe to include in support reports. Not an idempotency key.
  schemas:
    BatchAnalysisRequest:
      type: object
      required:
        - emails
      additionalProperties: false
      properties:
        emails:
          type: array
          minItems: 1
          maxItems: 50
          items:
            type: string
          description: Up to 50 emails per request. Each item consumes one request-unit.
        options:
          $ref: '#/components/schemas/AnalysisOptions'
    BatchAnalysisResponse:
      type: object
      required:
        - count
        - success_count
        - failure_count
        - results
      properties:
        count:
          type: integer
        success_count:
          type: integer
        failure_count:
          type: integer
        results:
          type: array
          items:
            $ref: '#/components/schemas/BatchResult'
        metadata:
          $ref: '#/components/schemas/ResponseMetadata'
    AnalysisOptions:
      type: object
      properties:
        include_explanation:
          type: boolean
          description: Request the explanation object. Subject to the explanation quota.
        include_checks:
          type: boolean
          description: Include the full checks object. (JSON key is include_checks.)
    BatchResult:
      type: object
      required:
        - index
        - success
        - email
        - decision
        - checks
        - explanation
        - error
      description: One batch item. Inapplicable objects are null.
      properties:
        index:
          type: integer
        success:
          type: boolean
        email:
          allOf:
            - $ref: '#/components/schemas/EmailInfo'
          nullable: true
        decision:
          allOf:
            - $ref: '#/components/schemas/Decision'
          nullable: true
        checks:
          allOf:
            - $ref: '#/components/schemas/Checks'
          nullable: true
        explanation:
          allOf:
            - $ref: '#/components/schemas/Explanation'
          nullable: true
        error:
          $ref: '#/components/schemas/ErrorDetailObject'
    ResponseMetadata:
      type: object
      required:
        - warnings
        - usage
        - plan
        - assessment_version
      properties:
        warnings:
          type: array
          items:
            $ref: '#/components/schemas/Warning'
        usage:
          $ref: '#/components/schemas/UsageSnapshot'
        plan:
          $ref: '#/components/schemas/PlanContext'
        assessment_version:
          type: string
        cache:
          $ref: '#/components/schemas/CacheInfo'
    PublicErrorEnvelope:
      type: object
      required:
        - error
      properties:
        error:
          $ref: '#/components/schemas/PublicError'
    EmailInfo:
      type: object
      required:
        - input
        - normalized
        - is_well_formed
        - format_issues
        - format_explanation
        - is_internationalized
      properties:
        input:
          type: string
        normalized:
          type: string
        normalized_preserving_case:
          type: string
          description: >-
            Address with the domain normalized identically to `normalized` but
            the local part preserving the submitted case (RFC 5321 §2.4 treats
            local parts as case-sensitive). Use `normalized` for deduplication;
            use this for display or exact echo.
        canonical:
          type: string
          description: >-
            Canonical identity form for deduplication: lowercased,
            googlemail.com folded to gmail.com, local part cut at the first '+',
            dots removed for gmail.com only. Use for per-identity dedup and rate
            limiting; NOT for delivery (at providers without subaddressing the
            plus-form may be a distinct mailbox).
        is_well_formed:
          type: boolean
        format_issues:
          type: array
          items:
            type: string
        format_explanation:
          type: string
        is_internationalized:
          type: boolean
          description: >-
            True when the normalized local part contains non-ASCII characters —
            an internationalized (EAI/SMTPUTF8, RFC 6531/6532) address. Metadata
            only: informational, not a check, not scored, and never a format
            issue. Deliverability caveat: receivers that do not support SMTPUTF8
            may bounce mail to these addresses.
    Decision:
      type: object
      required:
        - action
        - risk_score
        - risk_band
        - summary
        - assessment
      properties:
        action:
          type: string
          enum:
            - allow
            - review
            - block
          description: >-
            Recommended action for signup/lead flows, derived from the risk
            band: low -> allow, moderate -> review, high -> review, very_high ->
            block.
        risk_score:
          type: integer
          minimum: 0
          maximum: 100
          description: Deterministic 0-100 risk score for the submitted signup context.
        risk_band:
          type: string
          enum:
            - low
            - moderate
            - high
            - very_high
          description: 'Score band: low 0-24, moderate 25-49, high 50-74, very_high 75-100.'
        summary:
          type: string
        assessment:
          $ref: '#/components/schemas/Assessment'
    Checks:
      type: object
      description: |
        All sub-objects are optional (nullable); each is `null` when not run or
        not available on the caller's plan.
      properties:
        disposable_email:
          type:
            - object
            - 'null'
          properties:
            result:
              type: string
              enum:
                - confirmed
                - likely
                - suspicious
                - unknown
                - none
            is_disposable:
              type: boolean
            confidence:
              type: string
              enum:
                - high
                - medium
                - low
                - unknown
                - none
            method:
              type: string
            matched_domain:
              type:
                - string
                - 'null'
            evidence:
              type: array
              items:
                $ref: '#/components/schemas/EvidenceItem'
            risk_contribution:
              $ref: '#/components/schemas/RiskContribution'
        domain_infrastructure:
          type:
            - object
            - 'null'
          properties:
            result:
              type: string
            risk_level:
              type: string
            available:
              type: boolean
            confidence:
              type: string
            method:
              type: string
            lookup_status:
              type: string
              enum:
                - success
                - domain_not_found
                - timeout
                - dns_error
                - network_error
                - partial_failure
                - lookup_failed
                - skipped_invalid_input
                - skipped_empty_domain
                - blocked_internal_domain
            evidence:
              type: array
              items:
                $ref: '#/components/schemas/EvidenceItem'
            records:
              type:
                - object
                - 'null'
              properties:
                mx:
                  $ref: '#/components/schemas/DomainInfraRecord'
                spf:
                  $ref: '#/components/schemas/DomainInfraRecord'
                dmarc:
                  $ref: '#/components/schemas/DomainInfraRecord'
            reserved_special_use:
              type:
                - object
                - 'null'
              description: >
                Present only when the domain is an IANA/RFC reserved or
                special-use name (RFC 2606/6761 — example.com/.net/.org,
                .invalid, .test, .example, .localhost, and friends). Such
                domains can never be registered by any person or entity and can
                never receive email, so the address cannot belong to a real
                mailbox owner. Absent for ordinary registrable domains.
              properties:
                status:
                  type: string
                  enum:
                    - reserved
                matched_domain:
                  type:
                    - string
                    - 'null'
                risk_contribution:
                  $ref: '#/components/schemas/RiskContribution'
            registration_age:
              description: >
                RDAP-sourced domain registration age (SIGRISK-020). ALWAYS
                present (the data is surfaced even when it contributes no risk):
                available=false with a null age means the age could not be
                authoritatively determined (no RDAP service for the TLD,
                timeout, or lookup not run), never that the domain is
                suspicious. The +10 corroboration fires only when the domain is
                young (< 30 days per RDAP) AND the same request already carries
                disposable >= suspicious or typo >= probable_typo evidence — a
                young domain alone is surfaced WITHOUT score.
              properties:
                available:
                  type: boolean
                age_days:
                  type:
                    - integer
                    - 'null'
                registered_at:
                  type:
                    - string
                    - 'null'
                  format: date
                status:
                  type: string
                  enum:
                    - success
                    - not_registered
                    - not_determined
                    - unavailable
                    - unknown
                risk_contribution:
                  $ref: '#/components/schemas/RiskContribution'
            risk_contribution:
              $ref: '#/components/schemas/RiskContribution'
        provider:
          type:
            - object
            - 'null'
          properties:
            result:
              type: string
            confidence:
              type: string
            method:
              type: string
            type:
              type: string
            email_class:
              type: string
            business_email:
              type: boolean
            organization_email:
              type: boolean
            evidence:
              type: array
              items:
                $ref: '#/components/schemas/EvidenceItem'
            risk_contribution:
              $ref: '#/components/schemas/RiskContribution'
        role_address:
          type:
            - object
            - 'null'
          properties:
            result:
              type: string
              enum:
                - personal_like
                - functional_role
                - team_role
                - transactional_role
                - security_or_abuse_role
                - automated_or_no_reply
                - unknown
            is_role_account:
              type: boolean
            is_role_address:
              type: boolean
            role_category:
              type: string
            confidence:
              type: string
            method:
              type: string
            matched_role:
              type:
                - string
                - 'null'
            evidence:
              type: array
              items:
                $ref: '#/components/schemas/EvidenceItem'
            risk_contribution:
              $ref: '#/components/schemas/RiskContribution'
        domain_typo:
          type:
            - object
            - 'null'
          properties:
            result:
              type: string
              enum:
                - probable_typo
                - near_match
                - suspicious_similarity
                - no_typo
                - unknown
            is_typo:
              type: boolean
            is_typo_suspected:
              type: boolean
            confidence:
              type: string
            method:
              type: string
            matched_domain:
              type:
                - string
                - 'null'
            typo_domain:
              type:
                - string
                - 'null'
            suggested_domain:
              type:
                - string
                - 'null'
            evidence:
              type: array
              items:
                $ref: '#/components/schemas/EvidenceItem'
            risk_contribution:
              $ref: '#/components/schemas/RiskContribution'
        synthetic_identity:
          type:
            - object
            - 'null'
          properties:
            is_synthetic:
              type: boolean
            method:
              type: string
            matched_patterns:
              type: array
              items:
                type: object
                required:
                  - value
                  - type
                properties:
                  value:
                    type: string
                  type:
                    type: string
            confidence:
              type:
                - string
                - 'null'
            risk_contribution:
              $ref: '#/components/schemas/RiskContribution'
        name_match:
          type:
            - object
            - 'null'
          properties:
            result:
              type: string
            confidence:
              type: string
            method:
              type: string
            name_supplied:
              type: boolean
            evidence:
              type: array
              items:
                $ref: '#/components/schemas/EvidenceItem'
            sub_checks:
              type:
                - object
                - 'null'
              properties:
                name_email_alignment:
                  $ref: '#/components/schemas/NameMatchSubCheck'
                local_part_quality:
                  $ref: '#/components/schemas/NameMatchSubCheck'
                synthetic_name:
                  $ref: '#/components/schemas/NameMatchSubCheck'
            risk_contribution:
              $ref: '#/components/schemas/RiskContribution'
        ip_reputation:
          type:
            - object
            - 'null'
          properties:
            result:
              type: string
            risk_level:
              type: string
            available:
              type: boolean
            confidence:
              type: string
            method:
              type: string
            evidence:
              type: array
              items:
                $ref: '#/components/schemas/EvidenceItem'
            network:
              type:
                - object
                - 'null'
              properties:
                type:
                  type: string
                asn:
                  type: integer
                organization:
                  type: string
            risk_contribution:
              $ref: '#/components/schemas/RiskContribution'
    Explanation:
      type: object
      properties:
        explanation_available:
          type: boolean
        explanation_omitted_reason:
          type:
            - string
            - 'null'
        summary:
          type: string
        top_reasons:
          type: array
          items:
            type: object
            required:
              - reason
              - check
              - contribution
            properties:
              reason:
                type: string
              check:
                type: string
                enum:
                  - email_format
                  - disposable_email
                  - domain_infrastructure
                  - provider
                  - role_address
                  - domain_typo
                  - synthetic_identity
                  - name_match
                  - ip_reputation
              contribution:
                type: integer
        reason_groups:
          type:
            - object
            - 'null'
          properties:
            identity:
              type: array
              items:
                type: string
            infrastructure:
              type: array
              items:
                type: string
            behavior:
              type: array
              items:
                type: string
            format:
              type: array
              items:
                type: string
        risk_factors:
          type: array
          items:
            type: object
            required:
              - factor
              - contribution
            properties:
              factor:
                type: string
              contribution:
                type: integer
        neutral_factors:
          type: array
          items:
            type: object
            required:
              - factor
              - contribution
            properties:
              factor:
                type: string
              contribution:
                type: integer
        recommendation_reason:
          type: string
    ErrorDetailObject:
      type:
        - object
        - 'null'
      description: Per-item error on a failed batch item (null on success).
      properties:
        code:
          type: string
        message:
          type: string
        details:
          type: object
          additionalProperties: true
    Warning:
      type: object
      required:
        - code
        - message
        - type
        - upgrade_required
        - recommended_plan
      properties:
        code:
          type: string
        message:
          type: string
        type:
          type: string
        upgrade_required:
          type: boolean
        required_plan:
          type:
            - string
            - 'null'
        recommended_plan:
          type:
            - string
            - 'null'
    UsageSnapshot:
      type: object
      required:
        - requests
        - explanations
      properties:
        requests:
          $ref: '#/components/schemas/UsageDimension'
        explanations:
          $ref: '#/components/schemas/UsageDimension'
    PlanContext:
      type: object
      required:
        - current_plan
        - billing_period
        - reset_at
      properties:
        current_plan:
          type: string
        billing_period:
          $ref: '#/components/schemas/BillingPeriod'
        reset_at:
          type: string
    CacheInfo:
      type: object
      properties:
        hit:
          type: boolean
        age_seconds:
          type: integer
        scoring_version:
          type: string
    PublicError:
      type: object
      required:
        - code
        - message
        - type
        - request_id
        - retryable
      description: |
        The provider-neutral public error (DIRECT-011). The legacy
        `upgrade_url` field is intentionally absent; remediation is delivered
        via `action`. `ACCOUNT_DISABLED` is HTTP 403 (authorization), not 401.
      properties:
        code:
          $ref: '#/components/schemas/PublicErrorCode'
        message:
          type: string
        type:
          $ref: '#/components/schemas/PublicErrorType'
        request_id:
          type: string
        retryable:
          type: boolean
        details:
          type: object
          additionalProperties: true
          description: Optional safe, typed details (e.g. required_scope, required_plan).
        action:
          allOf:
            - $ref: '#/components/schemas/ErrorAction'
          description: Optional first-party remediation action.
        field_errors:
          type: array
          items:
            $ref: '#/components/schemas/FieldError'
        retry_after_seconds:
          type:
            - integer
            - 'null'
          description: Seconds to wait before retrying (quota/rate-limit/service).
        upgrade_required:
          type:
            - boolean
            - 'null'
          description: Legacy plan-upgrade flag (additive; prefer `action`).
        required_plan:
          type:
            - string
            - 'null'
          description: Legacy minimum plan for the gated feature.
        recommended_plan:
          type:
            - string
            - 'null'
          description: Legacy recommended plan for the gated feature.
    Assessment:
      type: object
      required:
        - confidence
        - evidence_strength
        - evidence_coverage
        - uncertainty
        - limitations
      description: >-
        Decision-quality assessment. `limitations` is always an array (never
        null).
      properties:
        confidence:
          type: string
          enum:
            - high
            - medium
            - low
        evidence_strength:
          type: string
          enum:
            - strong
            - moderate
            - weak
        evidence_coverage:
          type: string
          enum:
            - broad
            - partial
            - limited
        uncertainty:
          type: string
          enum:
            - low
            - moderate
            - high
        limitations:
          type: array
          maxItems: 5
          items:
            type: string
            enum:
              - domain_intelligence_unavailable
              - insufficient_independent_evidence
              - contradictory_evidence
              - decision_near_threshold
              - provider_classification_unknown
              - identity_evidence_inconclusive
              - name_not_supplied
              - ip_not_supplied
              - check_disabled_by_request
              - check_not_available_on_plan
    EvidenceItem:
      type: object
      required:
        - type
        - value
        - confidence
        - description
      properties:
        type:
          type: string
        value:
          type: string
        confidence:
          type: string
        description:
          type: string
    RiskContribution:
      type: integer
      description: This check's signed contribution to the risk score.
    DomainInfraRecord:
      type:
        - object
        - 'null'
      properties:
        status:
          type: string
          enum:
            - present
            - missing
            - not_checked
            - unknown
        risk_contribution:
          type: integer
    NameMatchSubCheck:
      type:
        - object
        - 'null'
      properties:
        result:
          type: string
        confidence:
          type: string
        method:
          type: string
        evidence:
          type: array
          items:
            $ref: '#/components/schemas/EvidenceItem'
    UsageDimension:
      type: object
      required:
        - used
        - limit
        - unlimited
        - remaining
      properties:
        used:
          type: integer
        limit:
          type:
            - integer
            - 'null'
          description: null when unlimited
        unlimited:
          type: boolean
        remaining:
          type: integer
    BillingPeriod:
      type: object
      required:
        - starts
        - ends
      properties:
        starts:
          type: string
        ends:
          type: string
    PublicErrorCode:
      type: string
      description: |
        Centrally registered public error code (DIRECT-011 registry plus the
        DIRECT-012 legacy surface codes). `UNAUTHORIZED` is intentionally not
        listed: it is unreachable and unpublished.
      enum:
        - MISSING_API_KEY
        - INVALID_API_KEY
        - REVOKED_API_KEY
        - EXPIRED_API_KEY
        - MALFORMED_API_KEY
        - MISSING_SESSION
        - INVALID_SESSION
        - EXPIRED_SESSION
        - INSUFFICIENT_SCOPE
        - FEATURE_NOT_AVAILABLE
        - PLAN_REQUIRED
        - ACCOUNT_DISABLED
        - USER_DISABLED
        - ACCOUNT_SYNC_PENDING
        - ADMIN_ROLE_REQUIRED
        - INVALID_REQUEST
        - INVALID_EMAIL
        - MALFORMED_EMAIL
        - INVALID_PAGINATION
        - INVALID_DATE_RANGE
        - INVALID_API_KEY_NAME
        - INVALID_API_KEY_SCOPES
        - INVALID_BILLING_REQUEST
        - DUPLICATE_REQUEST
        - METHOD_NOT_ALLOWED
        - LIMIT_EXCEEDED
        - PLAYGROUND_INVALID_REQUEST
        - CHECKER_INVALID_EMAIL
        - PUBLIC_PLAYGROUND_INVALID_REQUEST
        - MONTHLY_QUOTA_EXCEEDED
        - EXPLANATION_QUOTA_EXCEEDED
        - RATE_LIMIT_EXCEEDED
        - PUBLIC_CHECKER_RATE_LIMITED
        - EXPLANATION_LIMIT_EXCEEDED
        - PLAYGROUND_QUOTA_EXCEEDED
        - PLAYGROUND_RATE_LIMITED
        - CHECKER_RATE_LIMITED
        - PUBLIC_PLAYGROUND_RATE_LIMITED
        - PLAN_NOT_SELF_SERVICE
        - SUBSCRIPTION_EXISTS
        - CHECKOUT_PENDING
        - BILLING_NOT_CONFIGURED
        - BILLING_UNAVAILABLE
        - BILLING_GATEWAY_ERROR
        - API_KEY_NOT_FOUND
        - BULK_JOB_NOT_FOUND
        - WEBHOOK_NOT_FOUND
        - ACCOUNT_NOT_FOUND
        - NOT_FOUND
        - SERVICE_UNAVAILABLE
        - ANALYSIS_UNAVAILABLE
        - CHECKER_UNAVAILABLE
        - PLAYGROUND_UNAVAILABLE
        - USAGE_UNAVAILABLE
        - CHECKER_ANALYSIS_FAILED
        - PUBLIC_PLAYGROUND_UNAVAILABLE
        - PUBLIC_PLAYGROUND_ANALYSIS_FAILED
        - INTERNAL_ERROR
    PublicErrorType:
      type: string
      enum:
        - authentication_error
        - authorization_error
        - validation_error
        - quota_error
        - rate_limit_error
        - billing_error
        - conflict_error
        - not_found_error
        - service_error
        - internal_error
    ErrorAction:
      type: object
      required:
        - type
      properties:
        type:
          $ref: '#/components/schemas/ActionType'
        url:
          type:
            - string
            - 'null'
        label:
          type:
            - string
            - 'null'
        required_plan:
          type:
            - string
            - 'null'
    FieldError:
      type: object
      required:
        - field
        - code
        - message
      properties:
        field:
          type: string
        code:
          type: string
        message:
          type: string
    ActionType:
      type: string
      enum:
        - upgrade_plan
        - manage_billing
        - create_api_key
        - sign_in
        - contact_sales
        - retry_later
  responses:
    BadRequest:
      description: Validation error.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PublicErrorEnvelope'
    Unauthorized:
      description: Authentication error (missing/invalid API key).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PublicErrorEnvelope'
    Forbidden:
      description: >-
        Authorization error (insufficient scope, feature gate, disabled, or
        missing/insufficient admin role — ADMIN_ROLE_REQUIRED).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PublicErrorEnvelope'
    TooManyRequests:
      description: Quota or short-window rate limit exceeded.
      headers:
        Retry-After:
          schema:
            type: integer
          description: Seconds to wait before retrying.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PublicErrorEnvelope'
    InternalError:
      description: Internal error.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PublicErrorEnvelope'
  securitySchemes:
    ApiKeyBearer:
      type: http
      scheme: bearer
      bearerFormat: eisk_live_<public-id>_<secret>
      description: |
        First-party API key. Send the canonical key as a Bearer token:
        ```
        Authorization: Bearer eisk_live_eik_<24hex>_<64hex>
        ```
        Used on all API-client routes (`/v1/analyze-email`, batch, bulk,
        customer webhooks). Scopes (`analyze`, `bulk`, `webhooks`) authorize a
        route; the account plan authorizes premium functionality.

````