openapi: "3.1.0"
info:
  title: "FirstSales Public API"
  version: "1.0.0"
  description: "Read-only, unauthenticated JSON endpoints for email validation, DNS/deliverability diagnostics, domain intelligence, and AI-assisted cold-email content analysis on firstsales.io. Every endpoint here requires no API key — call it directly. All endpoints are IP-rate-limited; see each operation's description and `x-rateLimit` extension for its exact limit."
  contact:
    name: "FirstSales Support"
    email: "firstsales@support.communicate.so"
    url: "https://firstsales.io"
  license:
    name: "Terms of Service"
    url: "https://firstsales.io/terms-of-service/"
  termsOfService: "https://firstsales.io/terms-of-service/"
servers:
  - url: "https://firstsales.io"
    description: "Production"
security: []
tags:
  - name: "System"
    description: "Service health."
  - name: "Tutorials"
    description: "Public, no-auth tutorial content feed."
  - name: "Email"
    description: "Email address validation and risk checks."
  - name: "DNS"
    description: "DNS record and deliverability diagnostics."
  - name: "Domain"
    description: "Domain intelligence (age, records, subdomains)."
  - name: "Analyze"
    description: "AI-assisted cold-email content analysis."
paths:
  /api/health:
    get:
      operationId: "getHealth"
      summary: "Service health check"
      description: "Returns service status, ISO timestamp, and process uptime in seconds. Call this to confirm the API is reachable before calling other endpoints. Not rate-limited."
      tags:
        - "System"
      security: []
      responses:
        200:
          description: "Service is healthy."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"
        400:
          description: "Invalid request — bad input, missing field, or malformed value."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        429:
          description: "Rate limit exceeded for this endpoint."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        500:
          description: "Unexpected server error."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/public/tutorials:
    get:
      operationId: "listTutorials"
      summary: "List public tutorials"
      description: "Returns every published tutorial (slug, title, summary, steps, pro tips, FAQs) as structured JSON. Use this to enumerate available tutorial content before fetching one by slug. Not rate-limited; cached for 1 hour."
      tags:
        - "Tutorials"
      security: []
      parameters:
        - name: "locale"
          in: "query"
          required: false
          description: "BCP-47 locale to overlay translated title/summary/content onto the English canonical tutorial."
          schema:
            type: "string"
            example: "es"
      responses:
        200:
          description: "List of tutorials."
          content:
            application/json:
              schema:
                type: "object"
                properties:
                  tutorials:
                    type: "array"
                    items:
                      $ref: "#/components/schemas/PublicTutorial"
                required:
                  - "tutorials"
        400:
          description: "Invalid request — bad input, missing field, or malformed value."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        429:
          description: "Rate limit exceeded for this endpoint."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        500:
          description: "Unexpected server error."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/public/tutorials/{slug}:
    get:
      operationId: "getTutorialBySlug"
      summary: "Get one tutorial by slug"
      description: "Returns a single tutorial's full content (steps, pro tips, FAQs) by its slug. Call listTutorials first to discover valid slugs. Returns 404 (not this operation's 400/429/500 error shape but the same Error schema) if the slug is unknown. Not rate-limited; cached for 1 hour."
      tags:
        - "Tutorials"
      security: []
      parameters:
        - name: "slug"
          in: "path"
          required: true
          description: "Tutorial slug, e.g. `cold-email-deliverability`."
          schema:
            type: "string"
        - name: "locale"
          in: "query"
          required: false
          description: "BCP-47 locale to overlay translated content onto the English canonical tutorial."
          schema:
            type: "string"
            example: "es"
      responses:
        200:
          description: "The tutorial."
          content:
            application/json:
              schema:
                type: "object"
                properties:
                  tutorial:
                    $ref: "#/components/schemas/PublicTutorial"
                required:
                  - "tutorial"
        400:
          description: "Invalid request — bad input, missing field, or malformed value."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        404:
          description: "No tutorial exists for this slug."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        429:
          description: "Rate limit exceeded for this endpoint."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        500:
          description: "Unexpected server error."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/email/validate:
    post:
      operationId: "validateEmail"
      summary: "Validate an email address's deliverability"
      description: "Checks email format, then resolves the domain's DNS to confirm it exists and has MX records, and flags reserved/disposable/free-provider domains. Use this before sending outreach to filter out addresses that cannot receive mail. Rate limit: 20 requests/minute per IP."
      tags:
        - "Email"
      security: []
      x-rateLimit:
        limit: 20
        windowSeconds: 60
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: "object"
              properties:
                email:
                  type: "string"
                  format: "email"
              required:
                - "email"
      responses:
        200:
          description: "Validation result (still 200 even when the email is invalid — check `valid`)."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EmailValidationResult"
        400:
          description: "Invalid request — bad input, missing field, or malformed value."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        429:
          description: "Rate limit exceeded for this endpoint."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        500:
          description: "Unexpected server error."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/email/disposable:
    post:
      operationId: "checkDisposableEmail"
      summary: "Score an email domain for disposable/temporary-mailbox risk"
      description: "Runs six weighted checks (known disposable database, domain age via RDAP, MX validity, reputation, naming pattern, MX cross-reference) and returns a 0-100 score plus a safe/suspicious/disposable/high_risk classification. Use this to filter throwaway addresses out of a lead list. Rate limit: 20 requests/minute per IP."
      tags:
        - "Email"
      security: []
      x-rateLimit:
        limit: 20
        windowSeconds: 60
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: "object"
              properties:
                email:
                  type: "string"
                  format: "email"
              required:
                - "email"
      responses:
        200:
          description: "Disposable-risk assessment."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DisposableCheckResult"
        400:
          description: "Invalid request — bad input, missing field, or malformed value."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        429:
          description: "Rate limit exceeded for this endpoint."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        500:
          description: "Unexpected server error."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/email/catchall:
    post:
      operationId: "checkCatchAllDomain"
      summary: "Detect whether a domain accepts mail to any mailbox (catch-all)"
      description: "Sends live SMTP RCPT TO probes with random non-existent mailboxes at the domain's primary MX host and infers catch-all behavior from acceptance/rejection. Use this to avoid false-positive 'valid' verdicts on catch-all domains. Slower than the other email checks (real SMTP round-trips). Rate limit: 20 requests/minute per IP."
      tags:
        - "Email"
      security: []
      x-rateLimit:
        limit: 20
        windowSeconds: 60
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: "object"
              properties:
                domain:
                  type: "string"
                  example: "example.com"
              required:
                - "domain"
      responses:
        200:
          description: "Catch-all detection result."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CatchAllResult"
        400:
          description: "Invalid request — bad input, missing field, or malformed value."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        429:
          description: "Rate limit exceeded for this endpoint."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        500:
          description: "Unexpected server error."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/dns/check:
    post:
      operationId: "checkDnsRecord"
      summary: "Run one specific DNS/email-authentication/security check against a domain"
      description: "Single-check DNS diagnostic tool: pass a `checkType` (spf, dkim, dmarc, mx, general, blacklist, iprep, ptr, tls, bimi, arc, ns, txt, cname, a, aaaa, soa, whois, availability, propagation, authority, dnssec, deliverability, smtp, or port) and get back pass/warn/fail sub-checks plus a 0-100 score. Use `dns/validate` instead if you need SPF+DKIM+DMARC+MX together in one call. Rate limit: 10 requests/minute per IP."
      tags:
        - "DNS"
      security: []
      x-rateLimit:
        limit: 10
        windowSeconds: 60
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: "object"
              properties:
                domain:
                  type: "string"
                  description: "Domain name, or an IPv4 address when checkType is iprep/ptr."
                checkType:
                  type: "string"
                  enum:
                    - "spf"
                    - "dkim"
                    - "dmarc"
                    - "mx"
                    - "general"
                    - "blacklist"
                    - "iprep"
                    - "ptr"
                    - "tls"
                    - "bimi"
                    - "arc"
                    - "ns"
                    - "txt"
                    - "cname"
                    - "a"
                    - "aaaa"
                    - "soa"
                    - "whois"
                    - "availability"
                    - "propagation"
                    - "authority"
                    - "dnssec"
                    - "deliverability"
                    - "smtp"
                    - "port"
                selector:
                  type: "string"
                  description: "Optional DKIM selector (alphanumeric + hyphens, max 63 chars); only used when checkType is dkim."
              required:
                - "domain"
                - "checkType"
      responses:
        200:
          description: "Check result for the requested checkType."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DnsCheckResult"
        400:
          description: "Invalid request — bad input, missing field, or malformed value."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        429:
          description: "Rate limit exceeded for this endpoint."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        500:
          description: "Unexpected server error."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/dns/dkim:
    post:
      operationId: "checkDkimRecord"
      summary: "Look up and validate a domain's DKIM record"
      description: "Auto-detects common DKIM selectors (or uses the selector you pass in manual mode), then reports key type, estimated key strength, testing-mode flag, and validity. Use this for a DKIM-focused check with more detail than dns/check's generic dkim type. Rate limit: 10 requests/minute per IP."
      tags:
        - "DNS"
      security: []
      x-rateLimit:
        limit: 10
        windowSeconds: 60
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: "object"
              properties:
                domain:
                  type: "string"
                selector:
                  type: "string"
                  description: "DKIM selector, e.g. `google`. Required when mode is 'manual'."
                mode:
                  type: "string"
                  enum:
                    - "auto"
                    - "manual"
                  description: "Defaults to auto-detect of common selectors."
              required:
                - "domain"
      responses:
        200:
          description: "DKIM lookup result."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DkimResult"
        400:
          description: "Invalid request — bad input, missing field, or malformed value."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        404:
          description: "Domain does not exist or has no DNS records."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        429:
          description: "Rate limit exceeded for this endpoint."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        500:
          description: "Unexpected server error."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/dns/validate:
    post:
      operationId: "validateDnsAuthentication"
      summary: "Run SPF, DKIM, DMARC, and MX checks together with a confidence score"
      description: "Multi-check email-authentication validator: runs SPF/DKIM/DMARC/MX in parallel (or a subset via `checks`) and returns a combined confidence_score. Use this for a one-call deliverability audit instead of four separate dns/check calls. Rate limit: 10 requests/minute per IP."
      tags:
        - "DNS"
      security: []
      x-rateLimit:
        limit: 10
        windowSeconds: 60
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: "object"
              properties:
                domain:
                  type: "string"
                checks:
                  type: "array"
                  items:
                    type: "string"
                    enum:
                      - "spf"
                      - "dkim"
                      - "dmarc"
                      - "mx"
                  description: "Subset of checks to run. Defaults to all four."
                options:
                  type: "object"
                  properties:
                    dkimSelector:
                      type: "string"
                      description: "Optional DKIM selector override."
              required:
                - "domain"
      responses:
        200:
          description: "Combined authentication validation result."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DnsValidateResult"
        400:
          description: "Invalid request — bad input, missing field, or malformed value."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        404:
          description: "Domain does not exist or has no DNS records."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        429:
          description: "Rate limit exceeded for this endpoint."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        500:
          description: "Unexpected server error."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/dns/reverse:
    post:
      operationId: "reverseDnsLookup"
      summary: "Reverse-DNS (PTR) lookup for an IPv4 address, with forward-confirmation"
      description: "Looks up PTR hostnames for an IPv4 address and re-resolves each hostname's A records to check they point back to the original IP (forward-confirmed reverse DNS, a common sending-IP reputation signal). Rejects private/reserved IPs. Rate limit: 10 requests/minute per IP."
      tags:
        - "DNS"
      security: []
      x-rateLimit:
        limit: 10
        windowSeconds: 60
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: "object"
              properties:
                ip:
                  type: "string"
                  format: "ipv4"
                  example: "203.0.113.10"
              required:
                - "ip"
      responses:
        200:
          description: "Reverse DNS lookup result."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ReverseDnsResult"
        400:
          description: "Invalid request — bad input, missing field, or malformed value."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        429:
          description: "Rate limit exceeded for this endpoint."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        500:
          description: "Unexpected server error."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/domain/age:
    post:
      operationId: "getDomainAge"
      summary: "Get a domain's registration age and expiration via RDAP"
      description: "Queries RDAP (the modern WHOIS replacement) for registration date, expiration date, last-updated date, registrar, and status, and computes age in days/months/years. Use this to flag newly-registered domains, a common spam/fraud signal. Rate limit: 10 requests/minute per IP."
      tags:
        - "Domain"
      security: []
      x-rateLimit:
        limit: 10
        windowSeconds: 60
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: "object"
              properties:
                domain:
                  type: "string"
              required:
                - "domain"
      responses:
        200:
          description: "Domain age/registration details."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DomainAgeResult"
        400:
          description: "Invalid request — bad input, missing field, or malformed value."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        404:
          description: "Domain not found in the RDAP registry."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        429:
          description: "Rate limit exceeded for this endpoint."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        500:
          description: "Unexpected server error."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/domain/dns-records:
    post:
      operationId: "getDomainDnsRecords"
      summary: "Fetch every DNS record type for a domain in one call"
      description: "Queries A, AAAA, MX, NS, TXT, SOA, and CNAME records in parallel and returns them flattened, with a per-type summary count. Use this for a full DNS snapshot instead of calling dns/check once per record type. Rate limit: 10 requests/minute per IP."
      tags:
        - "Domain"
      security: []
      x-rateLimit:
        limit: 10
        windowSeconds: 60
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: "object"
              properties:
                domain:
                  type: "string"
              required:
                - "domain"
      responses:
        200:
          description: "All discovered DNS records for the domain."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DnsRecordsResult"
        400:
          description: "Invalid request — bad input, missing field, or malformed value."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        404:
          description: "Domain does not exist or has no DNS records."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        429:
          description: "Rate limit exceeded for this endpoint."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        500:
          description: "Unexpected server error."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/domain/subdomains:
    post:
      operationId: "findSubdomains"
      summary: "Discover subdomains via Certificate Transparency logs"
      description: "Queries crt.sh Certificate Transparency logs for every certificate issued for the domain and extracts unique subdomains from SAN entries. Passive/OSINT technique — no active scanning. Slower than other domain checks (crt.sh can be slow) and more tightly rate-limited: 5 requests/minute per IP."
      tags:
        - "Domain"
      security: []
      x-rateLimit:
        limit: 5
        windowSeconds: 60
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: "object"
              properties:
                domain:
                  type: "string"
              required:
                - "domain"
      responses:
        200:
          description: "Discovered subdomains."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SubdomainsResult"
        400:
          description: "Invalid request — bad input, missing field, or malformed value."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        429:
          description: "Rate limit exceeded for this endpoint."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        500:
          description: "Unexpected server error."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        502:
          description: "Failed to query Certificate Transparency logs (crt.sh unavailable)."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        504:
          description: "Certificate Transparency lookup timed out."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/analyze/spam:
    post:
      operationId: "analyzeSpamScore"
      summary: "Score email content for spam-trigger risk"
      description: "AI-assisted analysis (with a deterministic local fallback when no AI key is configured) that flags spam trigger words, excessive capitalization, urgency language, and misleading claims, returning a 0-100 spam-risk score and improvement suggestions. Content must be 10-10000 characters. Rate limit: 30 requests/minute per IP."
      tags:
        - "Analyze"
      security: []
      x-rateLimit:
        limit: 30
        windowSeconds: 60
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: "object"
              properties:
                content:
                  type: "string"
                  minLength: 10
                  maxLength: 10000
              required:
                - "content"
      responses:
        200:
          description: "Spam risk analysis."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SpamAnalysisResult"
        400:
          description: "Invalid request — bad input, missing field, or malformed value."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        429:
          description: "Rate limit exceeded for this endpoint."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        500:
          description: "Unexpected server error."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/analyze/subject-line:
    post:
      operationId: "analyzeSubjectLine"
      summary: "Score and improve a cold-email subject line"
      description: "AI-assisted analysis (with local fallback) returning a 0-100 effectiveness score, issues, strengths, improvement suggestions, and an optional rewritten version of the subject line. Rate limit: 30 requests/minute per IP."
      tags:
        - "Analyze"
      security: []
      x-rateLimit:
        limit: 30
        windowSeconds: 60
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: "object"
              properties:
                subjectLine:
                  type: "string"
              required:
                - "subjectLine"
      responses:
        200:
          description: "Subject line analysis."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ContentAnalysisResult"
        400:
          description: "Invalid request — bad input, missing field, or malformed value."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        429:
          description: "Rate limit exceeded for this endpoint."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        500:
          description: "Unexpected server error."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/analyze/cold-email:
    post:
      operationId: "analyzeColdEmail"
      summary: "Grade a full cold-email draft across 8 deliverability/effectiveness dimensions"
      description: "Local rule-based analysis (subject line, personalization, value proposition, CTA, length, tone, research signals, spam triggers) with an optional AI pass layered on top, returning a 0-100 score, letter grade, per-check detail, and per-dimension metrics. Content must be at least 20 characters. Not rate-limited."
      tags:
        - "Analyze"
      security: []
      x-rateLimit:
        limit: null
        windowSeconds: null
        note: "No rate limiter on this route."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: "object"
              properties:
                content:
                  type: "string"
                  minLength: 20
              required:
                - "content"
      responses:
        200:
          description: "Cold email grade and analysis."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ColdEmailAnalysisResult"
        400:
          description: "Invalid request — bad input, missing field, or malformed value."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        429:
          description: "Rate limit exceeded for this endpoint."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        500:
          description: "Unexpected server error."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/analyze/email-body:
    post:
      operationId: "analyzeEmailBody"
      summary: "Score and improve a cold-email body"
      description: "AI-assisted analysis (with local fallback) of email body copy, returning a 0-100 effectiveness score, issues, strengths, improvement suggestions, and an optional rewritten version. Rate limit: 30 requests/minute per IP."
      tags:
        - "Analyze"
      security: []
      x-rateLimit:
        limit: 30
        windowSeconds: 60
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: "object"
              properties:
                emailBody:
                  type: "string"
              required:
                - "emailBody"
      responses:
        200:
          description: "Email body analysis."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ContentAnalysisResult"
        400:
          description: "Invalid request — bad input, missing field, or malformed value."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        429:
          description: "Rate limit exceeded for this endpoint."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        500:
          description: "Unexpected server error."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/analyze/personalization:
    post:
      operationId: "analyzePersonalization"
      summary: "Detect and score personalization signals in cold-email content"
      description: "Pattern-matches for recipient-name tokens, company references, pain-point mentions, social proof, CTA, and value proposition; layers an optional AI pass on top; returns a 0-100 score plus per-signal booleans, strengths, and suggestions. Content must be at least 20 characters. Not rate-limited."
      tags:
        - "Analyze"
      security: []
      x-rateLimit:
        limit: null
        windowSeconds: null
        note: "No rate limiter on this route."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: "object"
              properties:
                content:
                  type: "string"
                  minLength: 20
              required:
                - "content"
      responses:
        200:
          description: "Personalization analysis."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PersonalizationAnalysisResult"
        400:
          description: "Invalid request — bad input, missing field, or malformed value."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        429:
          description: "Rate limit exceeded for this endpoint."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        500:
          description: "Unexpected server error."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
components:
  schemas:
    Error:
      type: "object"
      description: "Standard error envelope returned by every non-2xx response."
      properties:
        error:
          type: "string"
          description: "Machine-readable error code, e.g. `invalid_input`."
        message:
          type: "string"
          description: "Human-readable explanation."
        hint:
          type: "string"
          description: "How to fix the request."
      required:
        - "error"
        - "message"
        - "hint"
    HealthResponse:
      type: "object"
      properties:
        status:
          type: "string"
          enum:
            - "ok"
        timestamp:
          type: "string"
          format: "date-time"
        uptime:
          type: "number"
          description: "Process uptime in seconds."
      required:
        - "status"
        - "timestamp"
        - "uptime"
    PublicTutorial:
      type: "object"
      properties:
        slug:
          type: "string"
        title:
          type: "string"
        summary:
          type: "string"
        category:
          type: "string"
        industry:
          type: "string"
        difficulty:
          type: "string"
          enum:
            - "beginner"
            - "intermediate"
            - "advanced"
        estMinutes:
          type: "number"
        steps:
          type: "array"
          items:
            type: "object"
        proTips:
          type: "array"
          items:
            type: "object"
        faqs:
          type: "array"
          items:
            type: "object"
      required:
        - "slug"
        - "title"
        - "summary"
        - "category"
        - "difficulty"
        - "estMinutes"
        - "steps"
        - "proTips"
        - "faqs"
    MxRecord:
      type: "object"
      properties:
        exchange:
          type: "string"
        priority:
          type: "number"
      required:
        - "exchange"
        - "priority"
    EmailValidationResult:
      type: "object"
      properties:
        valid:
          type: "boolean"
        domain:
          type: "string"
        domainExists:
          type: "boolean"
        hasMx:
          type: "boolean"
        isReserved:
          type: "boolean"
        isDisposable:
          type: "boolean"
        isFreeProvider:
          type: "boolean"
        mxRecords:
          type: "array"
          items:
            $ref: "#/components/schemas/MxRecord"
        error:
          type: "string"
        message:
          type: "string"
      required:
        - "valid"
        - "domain"
        - "domainExists"
        - "hasMx"
        - "isReserved"
        - "isDisposable"
        - "isFreeProvider"
    DisposableCheckResult:
      type: "object"
      properties:
        score:
          type: "number"
          minimum: 0
          maximum: 100
        classification:
          type: "string"
          enum:
            - "safe"
            - "suspicious"
            - "disposable"
            - "high_risk"
        checks:
          type: "array"
          items:
            type: "object"
            properties:
              label:
                type: "string"
              status:
                type: "string"
                enum:
                  - "pass"
                  - "warn"
                  - "fail"
              detail:
                type: "string"
              weight:
                type: "number"
            required:
              - "label"
              - "status"
              - "detail"
              - "weight"
        details:
          type: "object"
          properties:
            domain:
              type: "string"
            isDisposable:
              type: "boolean"
            domainAge:
              type:
                - "object"
                - "null"
              properties:
                registeredAt:
                  type: "string"
                ageDays:
                  type: "number"
            mxRecords:
              type: "array"
              items:
                $ref: "#/components/schemas/MxRecord"
            hasMx:
              type: "boolean"
            patternMatch:
              type: "boolean"
            databaseMatch:
              type: "boolean"
      required:
        - "score"
        - "classification"
        - "checks"
        - "details"
    CatchAllResult:
      type: "object"
      properties:
        isCatchAll:
          type: "boolean"
        confidence:
          type: "string"
          enum:
            - "high"
            - "medium"
            - "low"
        checks:
          type: "array"
          items:
            type: "object"
            properties:
              label:
                type: "string"
              status:
                type: "string"
                enum:
                  - "pass"
                  - "warn"
                  - "fail"
              detail:
                type: "string"
            required:
              - "label"
              - "status"
              - "detail"
        score:
          type: "number"
          minimum: 0
          maximum: 100
      required:
        - "isCatchAll"
        - "confidence"
        - "checks"
        - "score"
    DnsCheckResult:
      type: "object"
      description: "Generic result shape shared by all dns/check checkTypes."
      properties:
        exists:
          type: "boolean"
        records:
          type: "array"
          items:
            type: "string"
        checks:
          type: "array"
          items:
            type: "object"
            properties:
              label:
                type: "string"
              status:
                type: "string"
                enum:
                  - "pass"
                  - "warn"
                  - "fail"
              detail:
                type: "string"
            required:
              - "label"
              - "status"
              - "detail"
        score:
          type: "number"
          minimum: 0
          maximum: 100
        error:
          type: "string"
        message:
          type: "string"
      required:
        - "exists"
    DkimResult:
      type: "object"
      properties:
        domain:
          type: "string"
        selector:
          type: "string"
        recordFound:
          type: "boolean"
        validStructure:
          type: "boolean"
        publicKeyPresent:
          type: "boolean"
        rawRecord:
          type: "string"
        status:
          type: "string"
          enum:
            - "Valid"
            - "Invalid"
            - "Not Found"
        details:
          type: "object"
          properties:
            versionTag:
              type: "boolean"
            keyType:
              type: "string"
            estimatedKeyBits:
              type:
                - "number"
                - "null"
            testingMode:
              type: "boolean"
            allSelectorsFound:
              type: "array"
              items:
                type: "string"
        message:
          type: "string"
      required:
        - "domain"
        - "selector"
        - "recordFound"
        - "validStructure"
        - "publicKeyPresent"
        - "rawRecord"
        - "status"
    DnsValidateResult:
      type: "object"
      properties:
        domain:
          type: "string"
        spf:
          type: "object"
        dkim:
          type: "object"
        dmarc:
          type: "object"
        mx:
          type: "object"
        confidence_score:
          type: "number"
          minimum: 0
          maximum: 100
        metadata:
          type: "object"
          properties:
            cached:
              type: "boolean"
            duration_ms:
              type: "number"
          required:
            - "cached"
            - "duration_ms"
      required:
        - "domain"
        - "metadata"
    ReverseDnsResult:
      type: "object"
      properties:
        ip:
          type: "string"
        hostnames:
          type: "array"
          items:
            type: "string"
        forwardResults:
          type: "array"
          items:
            type: "object"
            properties:
              hostname:
                type: "string"
              forwardIps:
                type: "array"
                items:
                  type: "string"
              matches:
                type: "boolean"
            required:
              - "hostname"
              - "forwardIps"
              - "matches"
        ptrError:
          type:
            - "string"
            - "null"
        ipClass:
          type: "object"
        networkType:
          type: "string"
      required:
        - "ip"
        - "hostnames"
        - "forwardResults"
        - "networkType"
    DomainAgeResult:
      type: "object"
      properties:
        domain:
          type: "string"
        registrationDate:
          type:
            - "string"
            - "null"
        expirationDate:
          type:
            - "string"
            - "null"
        lastUpdated:
          type:
            - "string"
            - "null"
        ageInDays:
          type:
            - "number"
            - "null"
        ageInMonths:
          type:
            - "number"
            - "null"
        ageInYears:
          type:
            - "number"
            - "null"
        registrar:
          type:
            - "string"
            - "null"
        status:
          type: "array"
          items:
            type: "string"
        name:
          type: "string"
      required:
        - "domain"
        - "registrationDate"
        - "expirationDate"
        - "lastUpdated"
        - "ageInDays"
        - "ageInMonths"
        - "ageInYears"
        - "registrar"
        - "status"
        - "name"
    DnsRecordsResult:
      type: "object"
      properties:
        domain:
          type: "string"
        records:
          type: "array"
          items:
            type: "object"
            properties:
              type:
                type: "string"
                enum:
                  - "A"
                  - "AAAA"
                  - "MX"
                  - "NS"
                  - "TXT"
                  - "SOA"
                  - "CNAME"
              value:
                type: "string"
              priority:
                type: "number"
              ttl:
                type: "number"
            required:
              - "type"
              - "value"
        summary:
          type: "object"
          description: "Count of records per type, e.g. { A: 1, MX: 2 }."
          additionalProperties:
            type: "number"
        totalRecords:
          type: "number"
      required:
        - "domain"
        - "records"
        - "summary"
        - "totalRecords"
    SubdomainsResult:
      type: "object"
      properties:
        domain:
          type: "string"
        subdomains:
          type: "array"
          items:
            type: "string"
        count:
          type: "number"
        source:
          type: "string"
          enum:
            - "Certificate Transparency (crt.sh)"
      required:
        - "domain"
        - "subdomains"
        - "count"
        - "source"
    SpamAnalysisResult:
      type: "object"
      properties:
        score:
          type: "number"
          minimum: 0
          maximum: 100
          description: "0 = clean, 100 = definitely spam."
        triggerWords:
          type: "array"
          items:
            type: "string"
        suggestions:
          type: "array"
          items:
            type: "string"
      required:
        - "score"
        - "triggerWords"
        - "suggestions"
    ContentAnalysisResult:
      type: "object"
      description: "Shared result shape for subject-line and email-body analysis."
      properties:
        score:
          type: "number"
          minimum: 0
          maximum: 100
        suggestions:
          type: "array"
          items:
            type: "string"
        issues:
          type: "array"
          items:
            type: "string"
        strengths:
          type: "array"
          items:
            type: "string"
        improvedVersion:
          type: "string"
      required:
        - "score"
        - "suggestions"
        - "issues"
        - "strengths"
    ColdEmailAnalysisResult:
      type: "object"
      properties:
        score:
          type: "number"
          minimum: 0
          maximum: 100
        grade:
          type: "string"
          enum:
            - "A"
            - "B"
            - "C"
            - "D"
            - "F"
        checks:
          type: "array"
          items:
            type: "object"
            properties:
              label:
                type: "string"
              passed:
                type: "boolean"
              detail:
                type: "string"
              scoreImpact:
                type: "number"
            required:
              - "label"
              - "passed"
              - "detail"
              - "scoreImpact"
        strengths:
          type: "array"
          items:
            type: "string"
        weaknesses:
          type: "array"
          items:
            type: "string"
        improvements:
          type: "array"
          items:
            type: "string"
        metrics:
          type: "object"
          properties:
            clarity:
              type: "number"
            personalization:
              type: "number"
            valueProp:
              type: "number"
            cta:
              type: "number"
            tone:
              type: "number"
        source:
          type: "string"
          enum:
            - "ai+local"
            - "local"
      required:
        - "score"
        - "grade"
        - "checks"
        - "strengths"
        - "weaknesses"
        - "improvements"
        - "metrics"
        - "source"
    PersonalizationAnalysisResult:
      type: "object"
      properties:
        score:
          type: "number"
          minimum: 0
          maximum: 100
        personalization:
          type: "object"
          properties:
            recipientName:
              type: "boolean"
            companyMention:
              type: "boolean"
            painPoint:
              type: "boolean"
            socialProof:
              type: "boolean"
            cta:
              type: "boolean"
            valueProp:
              type: "boolean"
        strengths:
          type: "array"
          items:
            type: "string"
        suggestions:
          type: "array"
          items:
            type: "string"
        checks:
          type: "array"
          items:
            type: "object"
            properties:
              label:
                type: "string"
              status:
                type: "string"
                enum:
                  - "pass"
                  - "warn"
                  - "fail"
              detail:
                type: "string"
            required:
              - "label"
              - "status"
              - "detail"
        source:
          type: "string"
          enum:
            - "ai+local"
            - "local"
      required:
        - "score"
        - "personalization"
        - "strengths"
        - "suggestions"
        - "checks"
        - "source"
