openapi: 3.0.3
info:
  title: desenmascara.me API
  version: "1.0"
  description: |
    Fraud-detection intelligence API. Analyze any domain and get a clear verdict
    (**LEGIT / SUSPICIOUS / FRAUDULENT**), a 0–100 risk score, structured signals,
    and a natural-language justification — cross-checked against alerts from 55+
    financial regulators worldwide, phishing feeds, Tranco, WHOIS and our own
    detection engines (brand & government impersonation, ClickFix, AI-agent traps,
    cloaking, phishing kits).

    ### Authentication
    All B2B endpoints use an API key sent in the `X-API-Key` header
    (format `dm_...`). Create and revoke keys in your [dashboard](https://desenmascara.me/dashboard).

    B2B API-key access also requires an **approved egress/source IP**.
    Non-approved IPs receive `403` with `error_code: api_ip_not_allowed`.
    Contact us to allowlist your IPs.

    ### Plans
    | Plan | Domain check | Fraud feed | Searches | Actor intel |
    |------|--------------|------------|----------|-------------|
    | Citizen (€39/mo) | 100/day | — | domain search | — |
    | Startup (€590/mo) | 10,000/month | ✓ | all | ✓ |
    | Enterprise | unlimited | ✓ | all | ✓ |

    Get a plan at [desenmascara.me/pro](https://desenmascara.me/pro).
  contact:
    name: desenmascara.me
    url: https://desenmascara.me/contact
servers:
  - url: https://desenmascara.me
security:
  - ApiKeyAuth: []
tags:
  - name: Domain Check
    description: On-demand fraud analysis of any domain.
  - name: Fraud Feed
    description: Pull-based incremental feed of newly recorded verdicts (Startup+).
  - name: Search
    description: Query the analyzed-domain corpus.
  - name: Threat Intel
    description: Actor clustering across shared infrastructure (Startup+).
  - name: Public
    description: Public, unauthenticated endpoints.
paths:
  /api/v1/unmask/:
    post:
      tags: [Domain Check]
      summary: Analyze a domain
      description: |
        Analyze a domain for fraud intent. Returns verdict, AI risk score (0–100),
        structured signals and justification.

        **Synchronous mode (default)**: blocks until the analysis is ready
        (typically 10–40s; cached results return instantly).

        **Asynchronous mode** (`"async": true`): returns `202 Accepted`
        immediately. Poll the same endpoint with the same body every
        `retry_after` seconds until you receive `200` with
        `analysis_status: "full"`.

        Plans: Citizen (100/day) · Startup (10,000/month) · Enterprise (unlimited).
      operationId: unmaskDomain
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domain]
              properties:
                domain:
                  type: string
                  description: Domain to analyze.
                  example: example.com
                async:
                  type: boolean
                  default: false
                  description: "`true` for async mode (202 + polling)."
                lang:
                  type: string
                  enum: [es, en]
                  description: Response language. Default auto-detect.
                force:
                  type: boolean
                  default: false
                  description: Force re-analysis (bypass cache).
                include_raw:
                  type: boolean
                  default: false
                  description: Include raw HTML and HTTP headers in the response.
                final_only:
                  type: boolean
                  default: true
                  description: Wait for the complete analysis (`false` may return preliminary results).
      responses:
        "200":
          description: Full analysis result.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AnalysisResult"
        "202":
          description: Analysis accepted and running in the background (async mode). Poll again after `retry_after` seconds.
          headers:
            Retry-After:
              schema: { type: integer }
              description: Seconds to wait before polling again.
          content:
            application/json:
              schema:
                type: object
                properties:
                  domain: { type: string, example: suspicious-site.com }
                  canonical_domain: { type: string }
                  analysis_status: { type: string, example: fast }
                  retry_after: { type: integer, example: 5 }
                  pending_started_at: { type: string, format: date-time }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/PlanRequired" }
        "403": { $ref: "#/components/responses/IpNotAllowed" }
        "429": { description: Rate limit exceeded. }
        "502":
          description: The site is unreachable (no HTTP response from any path). No verdict is issued for unreachable sites.
        "504":
          description: Analysis timed out, or the site did not respond in time.
  /api/account/fraud-feed/:
    get:
      tags: [Fraud Feed]
      summary: Incremental verdict feed
      description: |
        Pull-based incremental feed of newly recorded verdicts, designed for B2B
        integrations (ad platforms, signup funnels, payment risk, moderation,
        SOC automation).

        **Cursor flow**: start with `cursor=0`, store the returned `next_cursor`,
        and call again with `cursor=<next_cursor>` to fetch only newer items.

        Plans: Startup · Enterprise.
      operationId: fraudFeed
      parameters:
        - name: cursor
          in: query
          schema: { type: integer, default: 0 }
          description: Last seen cursor (exclusive).
        - name: limit
          in: query
          schema: { type: integer, default: 50, minimum: 1, maximum: 200 }
        - name: veredicts
          in: query
          schema: { type: string, default: FRAUDULENT }
          description: "Comma-separated verdicts: `FRAUDULENT`, `SUSPICIOUS`, `LEGIT`."
          example: FRAUDULENT,SUSPICIOUS
      responses:
        "200":
          description: Feed page.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        cursor: { type: integer, example: 12345 }
                        analyzed_at: { type: string, format: date-time }
                        domain: { type: string, example: example-scam.test }
                        veredict: { type: string, example: FRAUDULENT }
                        public_id: { type: string, format: uuid }
                        domain_ip: { type: string, nullable: true }
                        server_country: { type: string, nullable: true }
                        ai_score: { type: number, nullable: true }
                  next_cursor: { type: integer }
                  limit: { type: integer }
                  veredicts:
                    type: array
                    items: { type: string }
                  server_time: { type: string, format: date-time }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/PlanRequired" }
        "403": { $ref: "#/components/responses/IpNotAllowed" }
  /api/account/domain-search/:
    get:
      tags: [Search]
      summary: Search analyzed domains
      description: |
        Search the analyzed-domain corpus by (partial) domain name.

        Plans: Citizen · Startup · Enterprise.
      operationId: domainSearch
      parameters:
        - name: q
          in: query
          required: true
          schema: { type: string }
          description: Domain or substring to search.
          example: paypal
        - name: limit
          in: query
          schema: { type: integer, default: 50, minimum: 1 }
        - name: offset
          in: query
          schema: { type: integer, default: 0, minimum: 0 }
      responses:
        "200":
          description: Matching analyzed domains with verdicts and scores.
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/PlanRequired" }
        "403": { $ref: "#/components/responses/IpNotAllowed" }
  /api/account/fraud-type-search/:
    get:
      tags: [Search]
      summary: Search by fraud type
      description: |
        Search confirmed frauds by detected fraud type (e.g. `phishing`,
        `fake store`, `investment`).

        Plans: Startup · Enterprise.
      operationId: fraudTypeSearch
      parameters:
        - name: q
          in: query
          required: true
          schema: { type: string, maxLength: 80 }
          example: phishing
        - name: limit
          in: query
          schema: { type: integer, default: 12, minimum: 1, maximum: 50 }
        - name: offset
          in: query
          schema: { type: integer, default: 0, minimum: 0, maximum: 5000 }
      responses:
        "200":
          description: Matching fraudulent domains.
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/PlanRequired" }
        "403": { $ref: "#/components/responses/IpNotAllowed" }
  /api/actors/{actor_id}/:
    get:
      tags: [Threat Intel]
      summary: Actor cluster detail
      description: |
        Returns an actor cluster and its linked domains, grouped by shared
        infrastructure (IPs, SSL certificates, registrars).

        Plans: Startup · Enterprise (includes `pivot_evidence` infrastructure fingerprint).
      operationId: actorClusterDetail
      parameters:
        - name: actor_id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Cluster detail with related domains and infrastructure evidence.
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/PlanRequired" }
        "404": { description: Cluster not found. }
  /api/stats/:
    get:
      tags: [Public]
      summary: Platform statistics
      description: Public live counters (no authentication required).
      operationId: stats
      security: []
      responses:
        "200":
          description: Live platform counters.
          content:
            application/json:
              schema:
                type: object
                properties:
                  total_sites: { type: integer, example: 127000 }
                  fraud_sites: { type: integer }
                  total_phones: { type: integer }
components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: "API key from your dashboard (format: `dm_...`)."
  responses:
    Unauthorized:
      description: Missing or invalid API key.
    PlanRequired:
      description: Your plan does not include this endpoint.
    IpNotAllowed:
      description: "Source IP not approved for API access (`error_code: api_ip_not_allowed`)."
  schemas:
    AnalysisResult:
      type: object
      properties:
        domain: { type: string, example: example.com }
        canonical_domain: { type: string }
        requested_url: { type: string, example: "https://example.com" }
        fraudulent: { type: boolean }
        verificada:
          type: string
          enum: [LEGIT, SUSPICIOUS, FRAUDULENT]
        cached: { type: boolean }
        analysis_status: { type: string, example: full }
        analysis_public_id: { type: string, format: uuid }
        analysis_permalink: { type: string, example: /analisis/00000000-0000-0000-0000-000000000000 }
        analisis:
          type: object
          properties:
            veredict: { type: string, enum: [LEGIT, SUSPICIOUS, FRAUDULENT] }
            ai_score:
              type: number
              description: "Risk score 0–100 (0–30 green, 31–60 yellow, 61–100 red)."
              example: 5.0
            justification_ai: { type: string }
            citizen_signals:
              type: array
              items:
                type: object
                properties:
                  label_es: { type: string }
                  label_en: { type: string }
                  positive: { type: boolean, nullable: true }
            has_ssl: { type: boolean }
            domain_age_days: { type: integer, nullable: true }
            registrador: { type: string, nullable: true }
            screenshot: { type: string, example: "https://desenmascara.me/screens/abc123.png" }
            actor_cluster:
              type: object
              nullable: true
              properties:
                actor_id: { type: string }
                active_domains_count: { type: integer }
                target_brand: { type: string }
            vt_reported: { type: boolean }
            tranco_rank: { type: integer, nullable: true }
