openapi: 3.0.3
info:
  title: SignupSieve
  version: 1.0.0
  description: |
    SignupSieve: fake signup and email validation API. Catches fake humans, not just typos.

    One call tells you whether an address belongs to a real, unique person: a 0-100
    `human_score`, a verdict (`allow`, `review`, `block`) and the reasons. Checks never
    contact mail servers and addresses are never stored.

    Report what happened later with `POST /v1/feedback` and the model learns from it.
servers:
  - url: https://signupsieve.p.rapidapi.com
paths:
  /v1/check:
    get:
      summary: Check one address (query string)
      parameters:
        - name: email
          in: query
          required: true
          schema: { type: string, maxLength: 254, example: maria.santos@gmail.com }
        - name: ip
          in: query
          description: "Optional: the end user's public IP (cloud-server, Tor and same-IP checks). Never stored."
          schema: { type: string, example: 203.177.10.10 }
        - name: explain
          in: query
          description: true to include every signal's effect on the score
          schema: { type: boolean, default: false }
      responses:
        '200': { $ref: '#/components/responses/Check' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '503': { $ref: '#/components/responses/Unavailable' }
    post:
      summary: Check one address
      description: Preferred over GET so addresses stay out of URLs and logs.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              additionalProperties: false
              properties:
                email: { type: string, maxLength: 254, example: J.o.h.n+promo7@Gmial.com }
                explain: { type: boolean, default: false, description: Include every signal's effect on the score }
                ip: { type: string, example: 203.177.10.10, description: 'Optional: the end user''s public IP. Adds cloud-server, Tor and same-IP signup checks. Never stored.' }
      responses:
        '200': { $ref: '#/components/responses/Check' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '503': { $ref: '#/components/responses/Unavailable' }
  /v1/check/bulk:
    post:
      summary: Check up to 1,000 addresses
      description: Checked in parallel; addresses at the same domain share one DNS lookup. Results keep input order.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [emails]
              additionalProperties: false
              properties:
                emails:
                  type: array
                  minItems: 1
                  maxItems: 1000
                  items: { type: string }
      responses:
        '200':
          description: One result per input, in order
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      oneOf:
                        - $ref: '#/components/schemas/CheckResult'
                        - type: object
                          properties:
                            input: { type: string }
                            error: { type: string }
        '400': { $ref: '#/components/responses/BadRequest' }
  /v1/feedback:
    post:
      summary: Report what happened to a checked address
      description: |
        Teaches the model. `confirmed` and `real` mean the address belonged to a real person
        (verified email, logged in, paid). `fake`, `abuse` and `bounced` mean it didn't.
        Each check can be labelled once, within 14 days, by the account that made the check.
        Send the `check_id`, or just the `email`: that labels your latest check of that inbox
        (any spelling of it), so you don't need to store check IDs.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [outcome]
              additionalProperties: false
              properties:
                check_id: { type: string, example: chk_wa7opyn6znx7mdlkux7lrjhqi4 }
                email: { type: string, example: maria.santos@gmail.com, description: Instead of check_id }
                outcome: { type: string, enum: [confirmed, real, fake, abuse, bounced] }
      responses:
        '202': { description: Accepted for learning }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404':
          description: Unknown, expired or already-labelled check, or no check of this email in the last 14 days
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
  /v1/model:
    get:
      summary: Live model accuracy
      description: |
        Accuracy and log loss of the hand-tuned baseline and the learned model, measured on every
        feedback example before the model learned from it.
      responses:
        '200':
          description: Model stats
          content:
            application/json:
              schema:
                type: object
                properties:
                  mode: { type: string, enum: [off, on, auto] }
                  serving: { type: string, enum: [baseline, learned], description: 'What your account is served. Your personal model can start before the shared one, after about 50 of your own outcomes.' }
                  healthy: { type: boolean, description: Learned model is beating the baseline on recent feedback }
                  learned_version: { type: integer }
                  feedback_examples: { type: integer }
                  customers: { type: integer, description: Customers that have sent feedback }
                  baseline: { $ref: '#/components/schemas/ModelScore' }
                  learned: { $ref: '#/components/schemas/ModelScore' }
                  recommendation: { type: string }
components:
  responses:
    Check:
      description: Evidence for one address
      content:
        application/json:
          schema: { $ref: '#/components/schemas/CheckResult' }
    BadRequest:
      description: Invalid input
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Unavailable:
      description: Temporarily unavailable. Retry.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
  schemas:
    CheckResult:
      type: object
      required: [email, canonical, account_type, human_score, verdict, reasons, check_id, model]
      properties:
        email: { type: string, description: Trimmed and lowercased }
        canonical: { type: string, description: 'The inbox it really reaches: Gmail dots and +tags removed. Use it to spot one person making many accounts.' }
        did_you_mean: { type: string, description: Present when the domain looks like a typo }
        account_type: { type: string, enum: [work, free, school, relay, disposable, unknown] }
        role_account: { type: boolean, description: 'info@, support@ and similar' }
        disposable: { type: boolean }
        relay: { type: boolean, description: 'Privacy relay (Apple Hide My Email, Firefox Relay...): a real person who hides their address' }
        deliverable_domain: { type: boolean, nullable: true, description: Null when DNS could not be reached }
        mail_provider: { type: string, enum: [google, microsoft, zoho, proton, yahoo, apple, gateway, other, none] }
        domain_age_days: { type: integer, description: Present once the background check has run for this domain }
        domain_pattern: { type: string, enum: [match, mismatch, learning], description: 'Company domains only: does the address follow the shape (first.last...) the domain uses?' }
        alias_variants: { type: integer, description: 'Distinct spellings of this same inbox you have checked in the last 90 days (present when 2+). 3+ suggests one person making many accounts.' }
        recent_signups: { type: integer, description: 'Unique addresses at this domain checked in about the last 10 minutes, across all customers (present when 20+): a possible bot wave.' }
        domain_fake_rate: { type: number, description: 'Share of fake outcomes other customers reported at this domain (present once 2+ customers have reported).' }
        ip:
          type: object
          description: Present when the request included an ip.
          properties:
            datacenter: { type: string, example: aws, description: 'Cloud provider owning the IP (aws, google_cloud, oracle_cloud, digitalocean). Real people rarely sign up from a server.' }
            tor: { type: boolean }
            signups_from_ip: { type: integer, description: Distinct inboxes from this IP at your account in the last hour }
            ignored: { type: string, description: Why the IP was not used (private or invalid address) }
        evidence:
          type: array
          description: With explain=true. Each signal's effect on the score in log-odds; positive means more likely a real human.
          items:
            type: object
            properties:
              signal: { type: string, example: name_naturalness }
              value: { type: number }
              impact: { type: number }
        human_score: { type: integer, minimum: 0, maximum: 100 }
        verdict: { type: string, enum: [allow, review, block] }
        reasons: { type: array, items: { type: string }, example: [natural_name, matches_domain_address_pattern, work_domain] }
        check_id: { type: string, description: 'Signed token (about 120 characters). Pass to /v1/feedback within 14 days.' }
        feedback_wanted: { type: boolean, description: 'Present (true) when the model is unsure about this address. Reporting its outcome teaches the model the most.' }
        model: { type: string, example: baseline, description: '"baseline" or "learned-v<N>"' }
    ModelScore:
      type: object
      properties:
        accuracy: { type: number }
        log_loss: { type: number }
        recent_log_loss: { type: number, description: Moving average over roughly the last 50 examples }
    Error:
      type: object
      properties:
        error: { type: string }
