{
  "openapi": "3.1.0",
  "info": {
    "title": "MailSift API",
    "version": "1.0.0",
    "summary": "Real-time email verification and bulk CSV validation.",
    "description": "The MailSift API verifies email addresses in real time and processes bulk CSV jobs.\n\nAuthenticate every request with an API key in the `X-API-Key` header. Generate a key in the MailSift dashboard. The same endpoints also accept an authenticated session cookie when called from the dashboard, but API integrations should use an API key.\n\nAll responses are JSON unless noted (CSV download and the SSE progress stream). Errors use the shape `{ \"error\": \"message\" }`.",
    "contact": {
      "name": "MailSift Support",
      "url": "https://mailsift.dev/contact"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://mailsift.dev/terms"
    }
  },
  "externalDocs": {
    "description": "Human-readable API reference",
    "url": "https://mailsift.dev/api"
  },
  "servers": [
    { "url": "https://mailsift.dev/api/v1", "description": "Production" }
  ],
  "security": [{ "ApiKeyAuth": [] }],
  "tags": [
    { "name": "Verification", "description": "Single and batch email verification." },
    { "name": "Bulk Jobs", "description": "Asynchronous CSV verification jobs." },
    { "name": "Account", "description": "Credits, usage, and webhook configuration." }
  ],
  "paths": {
    "/verify": {
      "get": {
        "tags": ["Verification"],
        "operationId": "verifyEmail",
        "summary": "Verify a single email address",
        "description": "Runs syntax, MX, SPF, DMARC, disposable, role, catch-all and related checks for one address and returns a status, score, and the individual signals. Costs one credit.",
        "parameters": [
          {
            "name": "email",
            "in": "query",
            "required": true,
            "description": "Email address to verify (max 254 characters, RFC 5321).",
            "schema": { "type": "string", "format": "email", "maxLength": 254 },
            "example": "johnsmith@gmail.com"
          }
        ],
        "responses": {
          "200": {
            "description": "Verification result.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/VerifyResult" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/verify/batch": {
      "post": {
        "tags": ["Verification"],
        "operationId": "verifyBatch",
        "summary": "Verify up to 100 emails synchronously",
        "description": "Verifies an array of up to 100 addresses in a single call and returns a result per address. Each successfully verified address costs one credit; processing stops early (with `truncated: true`) if credits run out mid-batch.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/BatchRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Per-email results.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/BatchResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/verify/bulk": {
      "post": {
        "tags": ["Bulk Jobs"],
        "operationId": "createBulkJob",
        "summary": "Create a bulk verification job from a CSV",
        "description": "Uploads a CSV (multipart form field `file`) and enqueues an asynchronous job. The CSV must include a header row and a column whose name contains \"email\". Returns the created job immediately with `status: pending`; poll `GET /jobs/{id}` or stream `GET /jobs/{id}/stream` for progress.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": ["file"],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "CSV file with an email column and a header row. Max 32 MB."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Job created.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/BulkJob" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/jobs": {
      "get": {
        "tags": ["Bulk Jobs"],
        "operationId": "listJobs",
        "summary": "List bulk jobs",
        "description": "Returns the caller's bulk jobs, most recent first.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of jobs to return. Returns all jobs if omitted.",
            "schema": { "type": "integer", "minimum": 1 }
          }
        ],
        "responses": {
          "200": {
            "description": "List of jobs.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": { "$ref": "#/components/schemas/BulkJob" }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/jobs/{id}": {
      "parameters": [{ "$ref": "#/components/parameters/JobId" }],
      "get": {
        "tags": ["Bulk Jobs"],
        "operationId": "getJob",
        "summary": "Get a bulk job",
        "description": "Returns a job's summary and live progress counters (updated in real time by the worker while processing).",
        "responses": {
          "200": {
            "description": "Job summary.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/BulkJob" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      },
      "delete": {
        "tags": ["Bulk Jobs"],
        "operationId": "cancelJob",
        "summary": "Cancel or delete a bulk job",
        "description": "Cancels a pending/processing job, or deletes a finished job and its stored results.",
        "responses": {
          "200": {
            "description": "Job cancelled or deleted.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Message" },
                "examples": {
                  "cancelled": { "value": { "message": "job cancelled" } },
                  "deleted": { "value": { "message": "job deleted" } }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/jobs/{id}/results": {
      "parameters": [{ "$ref": "#/components/parameters/JobId" }],
      "get": {
        "tags": ["Bulk Jobs"],
        "operationId": "getJobResults",
        "summary": "Get parsed result rows",
        "description": "Returns paginated, parsed result rows for a completed job — useful for in-app filtering without downloading the full CSV.",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "1-indexed page number.",
            "schema": { "type": "integer", "minimum": 1, "default": 1 }
          },
          {
            "name": "page_size",
            "in": "query",
            "description": "Rows per page.",
            "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filter by verification status.",
            "schema": { "type": "string", "enum": ["valid", "risky", "invalid", "unknown"] }
          },
          {
            "name": "search",
            "in": "query",
            "description": "Case-insensitive substring match on the email address.",
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated result rows.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/JobResultsPage" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/jobs/{id}/download": {
      "parameters": [{ "$ref": "#/components/parameters/JobId" }],
      "get": {
        "tags": ["Bulk Jobs"],
        "operationId": "downloadJob",
        "summary": "Download the results CSV",
        "description": "Streams the final results CSV as a file attachment. Available once the job is complete.",
        "responses": {
          "200": {
            "description": "Results CSV.",
            "headers": {
              "Content-Disposition": {
                "description": "Attachment filename, e.g. attachment; filename=\"results-<original>.csv\".",
                "schema": { "type": "string" }
              }
            },
            "content": {
              "text/csv": { "schema": { "type": "string", "format": "binary" } }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/jobs/{id}/stream": {
      "parameters": [{ "$ref": "#/components/parameters/JobId" }],
      "get": {
        "tags": ["Bulk Jobs"],
        "operationId": "streamJobProgress",
        "summary": "Stream job progress (SSE)",
        "description": "Server-Sent Events stream that emits job progress (processed/valid/invalid/risky counts and status) until the job reaches a terminal state. Consume with an EventSource-compatible client.",
        "responses": {
          "200": {
            "description": "An event stream of job progress snapshots.",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string",
                  "description": "SSE frames whose `data:` payload is a JSON-encoded BulkJob snapshot."
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/account": {
      "get": {
        "tags": ["Account"],
        "operationId": "getAccount",
        "summary": "Get account, credits, and usage",
        "description": "Returns the current credit balance, usage for the calendar month, and (when present and authenticated via session) the webhook configuration. Some fields are omitted depending on the authentication mode.",
        "responses": {
          "200": {
            "description": "Account summary.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Account" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "API key generated in the MailSift dashboard. Format: msk_..."
      }
    },
    "parameters": {
      "JobId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Bulk job identifier (UUID).",
        "schema": { "type": "string", "format": "uuid" }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Invalid request.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "Unauthorized": {
        "description": "Missing or invalid credentials.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": "unauthorized" }
          }
        }
      },
      "PaymentRequired": {
        "description": "Insufficient credits.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": "insufficient credits" }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": "job not found" }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limit exceeded. See the X-RateLimit-* response headers.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "ServerError": {
        "description": "Unexpected server error.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": { "type": "string", "description": "Human-readable error message." }
        }
      },
      "Message": {
        "type": "object",
        "required": ["message"],
        "properties": {
          "message": { "type": "string" }
        }
      },
      "Checks": {
        "type": "object",
        "description": "Individual boolean signals that feed the score and status.",
        "properties": {
          "syntax": { "type": "boolean", "description": "Address is syntactically valid." },
          "free": { "type": "boolean", "description": "Free email provider (e.g. Gmail)." },
          "disposable": { "type": "boolean", "description": "Disposable/temporary provider." },
          "mx": { "type": "boolean", "description": "Domain has MX records." },
          "parked": { "type": "boolean", "description": "Domain appears parked." },
          "relay": { "type": "boolean", "description": "Address is an anonymized relay." },
          "spf": { "type": "boolean", "description": "Domain publishes an SPF record." },
          "dmarc": { "type": "boolean", "description": "Domain publishes a DMARC record." },
          "catch_all": { "type": "boolean", "description": "Domain accepts all addresses (catch-all)." },
          "role": { "type": "boolean", "description": "Role-based mailbox (e.g. support@)." }
        }
      },
      "VerifyResult": {
        "type": "object",
        "required": ["email", "status", "score", "checks", "domain"],
        "properties": {
          "email": { "type": "string" },
          "status": {
            "type": "string",
            "enum": ["valid", "risky", "invalid", "unknown"],
            "description": "Overall classification."
          },
          "score": { "type": "integer", "minimum": 0, "maximum": 100, "description": "Confidence/quality score." },
          "checks": { "$ref": "#/components/schemas/Checks" },
          "domain": { "type": "string" },
          "mx_records": { "type": "array", "items": { "type": "string" } },
          "free_check": { "type": "boolean" },
          "response_time_ms": { "type": "integer", "description": "Server-side verification time in milliseconds." },
          "checked_at": { "type": "string", "format": "date-time" },
          "normalized_email": { "type": "string", "description": "Canonicalized form of the address." },
          "did_you_mean": { "type": "string", "description": "Typo suggestion, when detected." },
          "mx_provider": { "type": "string", "description": "Recognized mail provider, e.g. \"Google\"." },
          "domain_age_days": { "type": "integer", "description": "Approximate domain age in days." },
          "dnsbl_listings": { "type": "array", "items": { "type": "string" } },
          "spf_record": { "type": "string" },
          "dmarc_record": { "type": "string" },
          "is_catch_all": { "type": "boolean" },
          "reasons": { "type": "array", "items": { "type": "string" } }
        },
        "examples": [
          {
            "email": "johnsmith@gmail.com",
            "status": "valid",
            "score": 92,
            "checks": {
              "syntax": true, "free": true, "disposable": false, "mx": true,
              "parked": false, "relay": false, "spf": true, "dmarc": true,
              "catch_all": false, "role": false
            },
            "domain": "gmail.com",
            "mx_records": ["gmail-smtp-in.l.google.com", "alt1.gmail-smtp-in.l.google.com"],
            "normalized_email": "johnsmith@gmail.com",
            "mx_provider": "Google",
            "is_catch_all": false,
            "domain_age_days": 11188,
            "response_time_ms": 46,
            "checked_at": "2026-04-01T17:32:18Z"
          }
        ]
      },
      "BatchRequest": {
        "type": "object",
        "required": ["emails"],
        "properties": {
          "emails": {
            "type": "array",
            "minItems": 1,
            "maxItems": 100,
            "items": { "type": "string", "format": "email" }
          }
        }
      },
      "BatchResultItem": {
        "type": "object",
        "required": ["email"],
        "properties": {
          "email": { "type": "string" },
          "result": { "$ref": "#/components/schemas/VerifyResult" },
          "error": { "type": "string", "description": "Present only when this address failed to verify." }
        }
      },
      "BatchResponse": {
        "type": "object",
        "required": ["results", "count", "credits_used", "truncated"],
        "properties": {
          "results": { "type": "array", "items": { "$ref": "#/components/schemas/BatchResultItem" } },
          "count": { "type": "integer", "description": "Number of results returned." },
          "credits_used": { "type": "integer" },
          "truncated": { "type": "boolean", "description": "True if processing stopped early due to credit depletion." }
        }
      },
      "BulkJob": {
        "type": "object",
        "required": ["id", "filename", "total_rows", "status", "created_at"],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "filename": { "type": "string" },
          "total_rows": { "type": "integer" },
          "processed_rows": { "type": "integer" },
          "valid_count": { "type": "integer" },
          "invalid_count": { "type": "integer" },
          "risky_count": { "type": "integer" },
          "unknown_count": { "type": "integer" },
          "status": {
            "type": "string",
            "enum": ["pending", "processing", "complete", "failed", "cancelled"]
          },
          "error_message": { "type": "string" },
          "started_at": { "type": ["string", "null"], "format": "date-time" },
          "completed_at": { "type": ["string", "null"], "format": "date-time" },
          "created_at": { "type": "string", "format": "date-time" }
        }
      },
      "JobResultRow": {
        "type": "object",
        "description": "A parsed result row.",
        "properties": {
          "email": { "type": "string" },
          "verification_status": { "type": "string" },
          "verification_score": { "type": "integer" },
          "is_disposable": { "type": "boolean" },
          "has_mx": { "type": "boolean" },
          "is_role_based": { "type": "boolean" },
          "is_free_provider": { "type": "boolean" },
          "is_relay": { "type": "boolean" },
          "is_blacklisted": { "type": "boolean" },
          "has_spf": { "type": "boolean" },
          "has_dmarc": { "type": "boolean" },
          "domain_age_days": { "type": "integer", "description": "Approximate domain age in days. Omitted when unknown." },
          "mx_provider": { "type": "string" },
          "normalized_email": { "type": "string" },
          "did_you_mean": { "type": "string" }
        }
      },
      "JobResultsPage": {
        "type": "object",
        "required": ["results", "total_rows", "page", "page_size", "total_pages"],
        "properties": {
          "results": { "type": "array", "items": { "$ref": "#/components/schemas/JobResultRow" } },
          "total_rows": { "type": "integer" },
          "page": { "type": "integer" },
          "page_size": { "type": "integer" },
          "total_pages": { "type": "integer" }
        }
      },
      "Usage": {
        "type": "object",
        "description": "Verification usage for the current calendar month.",
        "properties": {
          "year": { "type": "integer" },
          "month": { "type": "integer" },
          "total_checks": { "type": "integer" },
          "single_checks": { "type": "integer" },
          "bulk_checks": { "type": "integer" },
          "api_checks": { "type": "integer" },
          "public_checks": { "type": "integer" }
        }
      },
      "Account": {
        "type": "object",
        "required": ["credits", "usage", "auth_mode", "checked_at"],
        "properties": {
          "email": { "type": "string", "description": "Omitted when authenticated via API key." },
          "credits": { "type": "integer" },
          "is_admin": { "type": "boolean", "description": "Present only for admin accounts." },
          "webhook_url": { "type": "string", "description": "Configured webhook URL, when set." },
          "webhook_secret": {
            "type": "string",
            "description": "HMAC signing secret for webhook payloads. Omitted under API-key auth."
          },
          "usage": { "$ref": "#/components/schemas/Usage" },
          "auth_mode": { "type": "string", "enum": ["jwt", "api_key"] },
          "checked_at": { "type": "string", "format": "date-time" }
        }
      }
    }
  }
}
