openapi: "3.1.0"
info:
  title: "isitaspam.com — Spam Detection API"
  version: "1.0.0"
  description: |
    Free spam classification API powered by multi-LLM consensus (GPT, Claude, Gemini).
    Built from real moderation data across 51 Telegram groups protecting 29,000+ members.

    **Auth:** Pass `X-API-Key: varta_xxxx` to skip Turnstile and get higher rate limits.
    Free API key at https://isitaspam.com/developers — issued instantly, no email verification required.

    **Rate limits:**
    - Anonymous (no key): 15 checks/day per IP
    - Free API key: 50 checks/day
    - Micro ($5/mo): 300 checks/day
    - Starter ($19/mo): 2,000 checks/day
    - Pro ($49/mo): 10,000 checks/day

    **Latency:** p95 ~3s (LLM classification), ~30ms (cache hit for repeated text).
    Not suitable for per-message hot paths — use on join request queues or async moderation flows.

    **Parent product:** Varta — Telegram anti-spam bot (https://getvarta.com)
  contact:
    email: daryna@getvarta.com
  license:
    name: "API Terms of Service"
    url: "https://isitaspam.com/terms"

servers:
  - url: "https://isitaspam.com"
    description: "Production"

tags:
  - name: classification
    description: Spam detection endpoints
  - name: results
    description: Retrieve and manage past classification results
  - name: stats
    description: Aggregated statistics
  - name: keys
    description: API key management

paths:
  /api/check:
    post:
      operationId: classifyText
      summary: Classify a text message as spam / suspicious / safe
      description: |
        Analyzes a text message using 7-layer detection: fast pre-classifier, multi-LLM consensus
        (GPT + Claude + Gemini), vector similarity against 1,100+ verified scams, URL safety
        (Google Safe Browsing + PhishTank), crypto scam signals, language detection, and
        stylometric analysis (urgency density, caps ratio, mixed-script patterns).

        **Programmatic access:** include `X-API-Key` header to skip Turnstile and get higher limits.
        Without a key, browser clients must include a Cloudflare Turnstile token.
      tags: [classification]
      security:
        - apiKey: []
        - {}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [text]
              properties:
                text:
                  type: string
                  description: Message text to classify. Max 10,000 characters.
                  maxLength: 10000
                  example: "Earn $5000/week from home! Click here: bit.ly/abc123"
                locale:
                  type: string
                  description: "ISO 639-1 language hint (e.g. 'uk', 'ru', 'de'). Auto-detected if omitted."
                  example: "en"
                turnstile_token:
                  type: string
                  description: "Cloudflare Turnstile token. Required for browser clients without X-API-Key."
            examples:
              financial_scam:
                summary: Financial scam text
                value:
                  text: "Earn $5000/week from home! Limited spots — click now: bit.ly/abc123"
                  locale: "en"
              safe_message:
                summary: Legitimate message
                value:
                  text: "Hey everyone, our weekly meetup is on Thursday at 7pm. See you there!"
      responses:
        "200":
          description: Classification result
          headers:
            X-RateLimit-Limit:
              description: Daily quota for this key/IP
              schema: { type: integer }
            X-RateLimit-Remaining:
              description: Remaining checks in current UTC day
              schema: { type: integer }
            X-RateLimit-Reset:
              description: Unix timestamp (seconds) when quota resets
              schema: { type: integer }
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ClassificationResult"
              examples:
                spam_result:
                  summary: Spam detected
                  value:
                    slug: "a1b2c3d4"
                    url: "https://isitaspam.com/check/a1b2c3d4"
                    verdict: "DANGEROUS"
                    confidence: 0.97
                    category: "financial_scam"
                    signals: ["urgency_language", "income_claim", "suspicious_url"]
                    red_flags:
                      - "Unrealistic income claim ($5000/week)"
                      - "Shortened URL hides real destination"
                    what_to_do: "Delete message and ban sender. This is a financial scam."
                    language: "en"
                    meta:
                      cache_hit: "miss"
                      elapsed_ms: 2840
                      escalated: false
        "400":
          description: Invalid request
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
              examples:
                too_long:
                  value: { error: "text_too_long", message: "Text exceeds 10,000 character limit." }
                invalid_json:
                  value: { error: "invalid_json" }
        "403":
          description: Turnstile verification failed (browser clients only)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
              example: { error: "turnstile_failed" }
        "429":
          description: Rate limit exceeded
          headers:
            Retry-After:
              description: Seconds until next available request
              schema: { type: integer }
            X-RateLimit-Remaining:
              schema: { type: integer, example: 0 }
            X-RateLimit-Reset:
              schema: { type: integer }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/RateLimitError" }
              example:
                error: "rate_limit_exceeded"
                message: "Daily limit of 50 reached."
                reason: "day"
                reset_at: "2026-07-05T00:00:00Z"
                upgrade_url: "https://isitaspam.com/developers#pricing"
        "503":
          description: Service temporarily unavailable (LLM cost limit or upstream error)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
              example:
                error: "service_paused"
                message: "Daily AI budget reached. Service resumes at midnight UTC."

  /api/check/{slug}:
    get:
      operationId: getResult
      summary: Retrieve a past classification result by slug
      description: |
        Fetch a previously classified result. Results are permanent unless deleted by the user
        via GDPR right-to-erasure (/api/delete). Returns 410 Gone for deleted results.
      tags: [results]
      parameters:
        - name: slug
          in: path
          required: true
          schema: { type: string, example: "a1b2c3d4" }
          description: Unique result identifier returned by POST /api/check
      responses:
        "200":
          description: Classification result
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ClassificationResult"
                  - type: object
                    properties:
                      is_hidden:
                        type: boolean
                        description: Whether the message text was hidden by the user (verdict still visible)
                      is_indexed:
                        type: boolean
                        description: Whether this result is publicly indexed by search engines
                      feedback:
                        type: object
                        properties:
                          thumbs_up: { type: integer }
                          thumbs_down: { type: integer }
                      view_count: { type: integer }
                      created_at:
                        type: string
                        format: date-time
        "404":
          description: Result not found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
              example: { error: "not_found" }
        "410":
          description: Result was deleted (GDPR erasure)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
              example: { error: "gone" }

  /api/stats:
    get:
      operationId: getStats
      summary: Daily spam classification statistics
      description: |
        Aggregated stats for the current UTC day. Cached for 60 seconds.
        Use this to show live spam counters or understand current spam rates.
      tags: [stats]
      responses:
        "200":
          description: Daily stats
          headers:
            Cache-Control:
              schema: { type: string, example: "public, max-age=30" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  total: { type: integer, description: "Total classifications today" }
                  scams: { type: integer, description: "Messages classified as DANGEROUS" }
                  suspicious: { type: integer, description: "Messages classified as SUSPICIOUS" }
                  as_of:
                    type: string
                    format: date-time
                    description: "Timestamp of this snapshot"
              example:
                total: 1240
                scams: 847
                suspicious: 203
                as_of: "2026-07-04T14:30:00Z"

  /api/recent:
    get:
      operationId: getRecent
      summary: Five most recent public classification results
      description: |
        Returns the 5 most recent results that users have opted into public indexing.
        Cached for 60 seconds. Use for a live spam feed or "recent catches" widget.
      tags: [stats]
      responses:
        "200":
          description: Recent results
          headers:
            Cache-Control:
              schema: { type: string, example: "public, max-age=30" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        slug: { type: string }
                        verdict: { $ref: "#/components/schemas/Verdict" }
                        category: { $ref: "#/components/schemas/Category" }
                        created_at: { type: integer, description: "Unix ms timestamp" }
                        url: { type: string, format: uri }

  /api/keys:
    post:
      operationId: createApiKey
      summary: Get a free API key
      description: |
        Issues an API key instantly — no email verification required to start using it.
        The key is shown ONCE in the response — save it, it cannot be recovered.

        Free tier: 50 checks/day. Paid tiers at https://isitaspam.com/developers#pricing.
        Paid limits are activated automatically after Stripe checkout.
      tags: [keys]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
                  example: "dev@example.com"
      responses:
        "200":
          description: API key created
          content:
            application/json:
              schema:
                type: object
                properties:
                  key:
                    type: string
                    description: "API key — shown ONCE. Save it."
                    example: "varta_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4"
                  prefix:
                    type: string
                    description: "First 8 chars for display/identification"
                    example: "varta_a1"
                  daily_limit:
                    type: integer
                    example: 50
                  note:
                    type: string
                    example: "Key shown once — save it. Paid limits activate automatically after checkout."
        "400":
          description: Invalid email
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "429":
          description: Too many key creation attempts from this IP/email domain
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /api/feedback:
    post:
      operationId: submitFeedback
      summary: Correct a classification verdict
      description: |
        Submit a correction for a past result. Opted-in corrections feed Varta's training corpus
        — the labeled dataset that makes the classifier improve over time.
        Requires an API key.
      tags: [results]
      security:
        - apiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [slug]
              properties:
                slug:
                  type: string
                  example: "a1b2c3d4"
                correct:
                  type: boolean
                  description: "Was the verdict correct?"
                  example: false
                correction:
                  $ref: "#/components/schemas/Verdict"
                  description: "What the verdict should have been (if incorrect)"
      responses:
        "200":
          description: Feedback recorded
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean, example: true }
        "401":
          description: API key required
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
              example: { error: "api_key_required" }
        "404":
          description: Slug not found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

components:
  securitySchemes:
    apiKey:
      type: apiKey
      in: header
      name: X-API-Key
      description: "Free key at https://isitaspam.com/developers. Format: varta_xxxxxxxx"

  schemas:
    Verdict:
      type: string
      enum: [SAFE, SUSPICIOUS, DANGEROUS]
      description: |
        - DANGEROUS: High-confidence scam/spam. Delete and ban sender immediately.
        - SUSPICIOUS: Borderline. Review manually before acting.
        - SAFE: No spam signals detected.

    Category:
      type: string
      enum:
        - financial_scam
        - phishing
        - crypto_scam
        - fake_job
        - adult_content
        - self_promotion
        - fake_giveaway
        - impersonation
        - malware
        - other
        - clean

    ClassificationResult:
      type: object
      properties:
        slug:
          type: string
          description: Unique result ID. Stable — use to retrieve this result later.
          example: "a1b2c3d4"
        url:
          type: string
          format: uri
          description: Permanent public URL for this result (if user opts into indexing)
          example: "https://isitaspam.com/check/a1b2c3d4"
        verdict:
          $ref: "#/components/schemas/Verdict"
        confidence:
          type: number
          minimum: 0
          maximum: 1
          description: Classifier confidence score. Capped at 0.95 to avoid overconfidence.
          example: 0.97
        category:
          $ref: "#/components/schemas/Category"
        signals:
          type: array
          items: { type: string }
          description: Machine-readable risk signals detected
          example: ["urgency_language", "income_claim", "suspicious_url"]
        red_flags:
          type: array
          items: { type: string }
          description: Human-readable explanations of why this was flagged
          example: ["Unrealistic income claim", "Shortened URL hides destination"]
        what_to_do:
          type: string
          description: Plain-language recommended action
          example: "Delete message and ban sender. This is a financial scam."
        language:
          type: string
          description: Detected language (ISO 639-1)
          example: "en"
        meta:
          type: object
          description: Classification metadata (informational, may change)
          properties:
            cache_hit:
              type: string
              enum: [exact, similarity, miss, exact_post_llm, race_recovered]
              description: How this result was produced
            elapsed_ms:
              type: integer
              description: Total processing time in milliseconds
            escalated:
              type: boolean
              description: Whether higher-tier LLM models were used

    Error:
      type: object
      properties:
        error:
          type: string
          description: Machine-readable error code
        message:
          type: string
          description: Human-readable explanation

    RateLimitError:
      allOf:
        - $ref: "#/components/schemas/Error"
        - type: object
          properties:
            reason:
              type: string
              enum: [minute, hour, day, account]
            reset_at:
              type: string
              format: date-time
            upgrade_url:
              type: string
              format: uri
