{
  "openapi": "3.1.0",
  "info": {
    "title": "InboxGuard API",
    "version": "1.0.0",
    "summary": "Email deliverability monitoring, DMARC reporting, and DNS authentication scanning.",
    "description": "The InboxGuard REST API lets developers and AI agents run on-demand deliverability scans (SPF, DKIM, DMARC, MTA-STS, TLS-RPT, BIMI, and DNS blocklists), manage monitored domains, read DMARC aggregate reports, list alerts, and manage API keys.\n\n## Authentication\nAuthenticated endpoints accept an InboxGuard API key as a bearer token:\n\n```\nAuthorization: Bearer ig_live_xxxxxxxxxxxxxxxxxxxxxxxx\n```\n\nMint keys in the dashboard (Settings → API keys) or via `POST /api-keys`. Keys carry scopes: `read` (GET only) or `full` (all methods). See https://inboxguard.io/auth.md.\n\n## Errors\nEvery error returns the same structured envelope (`#/components/schemas/Error`): `{ \"error\": { \"code\": \"RATE_LIMIT\", \"message\": \"...\", \"requestId\": \"...\" } }`.\n\n## Rate limits\nThe anonymous `POST /scan-domain` endpoint allows 5 requests/hour per IP. Every response carries `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset` (and `X-RateLimit-*` equivalents); 429 responses add `Retry-After`.\n\n## Idempotency\nPOST requests accept an optional `Idempotency-Key` header, echoed on the response so clients can retry safely.\n\n## Versioning\nThe current version is `1.0` (also sent as the `API-Version` response header; clients may request a version with the optional `Accept-Version` header). Breaking changes ship under a new major version. Deprecations are announced via the `Deprecation` and `Sunset` response headers at least 90 days in advance.\n\n## Pagination\nList endpoints use cursor pagination: pass `?cursor=` (and optional `?limit=`); the response returns `next_cursor` (null when complete) and a `Link: <…>; rel=\"next\"` header.",
    "termsOfService": "https://inboxguard.io/terms",
    "contact": { "name": "InboxGuard Support", "email": "support@inboxguard.io", "url": "https://inboxguard.io/contact" },
    "license": { "name": "Proprietary", "url": "https://inboxguard.io/terms" }
  },
  "servers": [{ "url": "https://api.inboxguard.io", "description": "Production (v1)" }],
  "externalDocs": { "description": "API quickstart and guides", "url": "https://inboxguard.io/guides/api-quickstart" },
  "tags": [
    { "name": "Scanning", "description": "Run deliverability and authentication scans." },
    { "name": "Domains", "description": "Manage monitored domains." },
    { "name": "Reports", "description": "DMARC aggregate reports and scan history." },
    { "name": "Alerts", "description": "Posture-change alerts." },
    { "name": "Account", "description": "API keys and account metadata." },
    { "name": "System", "description": "Health and status." }
  ],
  "security": [{ "ApiKeyBearer": [] }],
  "paths": {
    "/health": {
      "get": {
        "operationId": "getHealth",
        "tags": ["System"],
        "summary": "Liveness and database connectivity probe",
        "description": "Unauthenticated. Returns service status and database connectivity.",
        "security": [],
        "parameters": [{ "$ref": "#/components/parameters/AcceptVersion" }],
        "responses": {
          "200": { "description": "Service healthy", "headers": { "API-Version": { "$ref": "#/components/headers/ApiVersion" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Health" }, "example": { "status": "ok", "db": "ok", "durationMs": 7, "version": "a1b2c3d" } } } },
          "503": { "description": "Service degraded (database unreachable)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Health" } } } }
        }
      }
    },
    "/scan-domain": {
      "post": {
        "operationId": "scanDomain",
        "tags": ["Scanning"],
        "summary": "Run a deliverability scan for a domain",
        "description": "Evaluates SPF (with 10-DNS-lookup budget), DKIM selectors, DMARC posture and alignment, MTA-STS, TLS-RPT, MX TLS, BIMI/VMC, and DNS blocklists, then returns a unified score. Works anonymously (5/hour per IP, free-tier blocklist subset) or authenticated (full set, persisted).",
        "security": [{}, { "ApiKeyBearer": [] }],
        "parameters": [
          { "$ref": "#/components/parameters/IdempotencyKey" },
          { "$ref": "#/components/parameters/AcceptVersion" }
        ],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ScanRequest" }, "example": { "domain": "example.com", "dkimSelectors": ["selector1", "google"] } } } },
        "responses": {
          "200": {
            "description": "Scan completed",
            "headers": {
              "RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
              "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" },
              "Idempotency-Key": { "$ref": "#/components/headers/IdempotencyKeyEcho" },
              "API-Version": { "$ref": "#/components/headers/ApiVersion" }
            },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ScanResult" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/ServiceUnavailable" }
        }
      }
    },
    "/domains": {
      "get": {
        "operationId": "listDomains",
        "tags": ["Domains"],
        "summary": "List monitored domains",
        "description": "Returns all domains monitored by the caller's organization, with their latest score and posture summary. Cursor-paginated.",
        "parameters": [{ "$ref": "#/components/parameters/Cursor" }, { "$ref": "#/components/parameters/Limit" }],
        "responses": {
          "200": { "description": "Domain list", "headers": { "Link": { "$ref": "#/components/headers/LinkNext" }, "API-Version": { "$ref": "#/components/headers/ApiVersion" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DomainList" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/domains/{id}": {
      "parameters": [{ "$ref": "#/components/parameters/DomainId" }],
      "get": {
        "operationId": "getDomain",
        "tags": ["Domains"],
        "summary": "Get a monitored domain",
        "description": "Returns the full posture detail and latest scan for a single monitored domain.",
        "responses": {
          "200": { "description": "Domain detail", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Domain" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      },
      "delete": {
        "operationId": "deleteDomain",
        "tags": ["Domains"],
        "summary": "Stop monitoring a domain",
        "description": "Removes a domain from monitoring. Requires the `full` scope.",
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "responses": {
          "200": { "description": "Deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OkResult" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/scans": {
      "get": {
        "operationId": "listScans",
        "tags": ["Reports"],
        "summary": "List recent scans",
        "description": "Returns recent scan results for the caller's organization, most recent first. Cursor-paginated.",
        "parameters": [
          { "name": "domain", "in": "query", "required": false, "description": "Filter by domain name.", "schema": { "type": "string" } },
          { "$ref": "#/components/parameters/Cursor" },
          { "$ref": "#/components/parameters/Limit" }
        ],
        "responses": {
          "200": { "description": "Scan history", "headers": { "Link": { "$ref": "#/components/headers/LinkNext" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ScanList" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/dmarc-reports": {
      "get": {
        "operationId": "listDmarcReports",
        "tags": ["Reports"],
        "summary": "List DMARC aggregate reports",
        "description": "Returns parsed DMARC aggregate (RUA) report summaries, broken down by source IP and authentication result. Cursor-paginated.",
        "parameters": [
          { "name": "domain", "in": "query", "required": false, "description": "Filter by domain name.", "schema": { "type": "string" } },
          { "$ref": "#/components/parameters/Cursor" },
          { "$ref": "#/components/parameters/Limit" }
        ],
        "responses": {
          "200": { "description": "DMARC report summaries", "headers": { "Link": { "$ref": "#/components/headers/LinkNext" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DmarcReportList" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/alerts": {
      "get": {
        "operationId": "listAlerts",
        "tags": ["Alerts"],
        "summary": "List alerts",
        "description": "Returns posture-change alerts for the caller's organization. Cursor-paginated.",
        "parameters": [
          { "name": "status", "in": "query", "required": false, "description": "Filter by alert status.", "schema": { "type": "string", "enum": ["open", "resolved"] } },
          { "$ref": "#/components/parameters/Cursor" },
          { "$ref": "#/components/parameters/Limit" }
        ],
        "responses": {
          "200": { "description": "Alert list", "headers": { "Link": { "$ref": "#/components/headers/LinkNext" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AlertList" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api-keys": {
      "get": {
        "operationId": "listApiKeys",
        "tags": ["Account"],
        "summary": "List API keys",
        "description": "Lists non-revoked API keys for the caller's organization. Secrets are never returned after creation.",
        "responses": {
          "200": { "description": "API keys", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiKeyList" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      },
      "post": {
        "operationId": "createApiKey",
        "tags": ["Account"],
        "summary": "Create an API key",
        "description": "Mints a new API key and returns the secret exactly once. Requires session (owner/admin) auth. Choose scopes `read` or `full`.",
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateApiKeyRequest" }, "example": { "name": "CI deploy bot", "environment": "live", "scopes": ["read"] } } } },
        "responses": {
          "201": { "description": "Key created (secret returned once)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiKeyWithSecret" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      }
    },
    "/api-keys/{id}": {
      "parameters": [{ "name": "id", "in": "path", "required": true, "description": "API key id (UUID).", "schema": { "type": "string", "format": "uuid" } }],
      "delete": {
        "operationId": "revokeApiKey",
        "tags": ["Account"],
        "summary": "Revoke an API key",
        "description": "Soft-revokes an API key. The key stops working immediately.",
        "responses": {
          "200": { "description": "Revoked", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OkResult" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyBearer": { "type": "http", "scheme": "bearer", "bearerFormat": "InboxGuard API key (ig_live_… / ig_test_…)", "description": "PRIMARY auth: send an InboxGuard API key as a bearer token (`Authorization: Bearer ig_live_…`). Scopes: `read` (GET) and `full` (all methods). Mint a key at https://inboxguard.io/settings or per https://inboxguard.io/auth.md." },
      "OAuth2": { "type": "oauth2", "description": "Discovery only. InboxGuard has no interactive OAuth token exchange — the credential is an API key (see ApiKeyBearer). This block exists so agents can auto-discover scopes and the RFC 8414/9728 metadata at /.well-known/oauth-authorization-server. The agent_auth flow there issues an api_key; see https://inboxguard.io/auth.md.", "flows": { "clientCredentials": { "tokenUrl": "https://api.inboxguard.io/agent/register", "scopes": { "read": "Read-only access (GET).", "full": "Full read/write access (all methods)." } } } }
    },
    "parameters": {
      "DomainId": { "name": "id", "in": "path", "required": true, "description": "Monitored domain id (UUID).", "schema": { "type": "string", "format": "uuid" } },
      "Cursor": { "name": "cursor", "in": "query", "required": false, "description": "Opaque cursor from a previous response's `next_cursor`. Omit for the first page.", "schema": { "type": "string" } },
      "Limit": { "name": "limit", "in": "query", "required": false, "description": "Max items to return (default 50, max 200).", "schema": { "type": "integer", "minimum": 1, "maximum": 200, "default": 50 } },
      "IdempotencyKey": { "name": "Idempotency-Key", "in": "header", "required": false, "description": "Client-generated key to make a retried POST safe. Echoed back on the response.", "schema": { "type": "string", "maxLength": 255 } },
      "AcceptVersion": { "name": "Accept-Version", "in": "header", "required": false, "description": "Request a specific API major version. Defaults to the current version.", "schema": { "type": "string", "enum": ["1.0"], "default": "1.0" } }
    },
    "headers": {
      "ApiVersion": { "description": "API version that served this response.", "schema": { "type": "string", "example": "1.0" } },
      "RateLimitLimit": { "description": "Request quota for the current window.", "schema": { "type": "integer" } },
      "RateLimitRemaining": { "description": "Requests remaining in the current window.", "schema": { "type": "integer" } },
      "RateLimitReset": { "description": "Seconds until the window resets.", "schema": { "type": "integer" } },
      "RetryAfter": { "description": "Seconds to wait before retrying.", "schema": { "type": "integer" } },
      "IdempotencyKeyEcho": { "description": "Echo of the request's Idempotency-Key, if provided.", "schema": { "type": "string" } },
      "LinkNext": { "description": "RFC 8288 link to the next page, when more results exist.", "schema": { "type": "string", "example": "<https://api.inboxguard.io/scans?cursor=abc>; rel=\"next\"" } }
    },
    "responses": {
      "BadRequest": { "description": "Invalid request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Unauthorized": { "description": "Missing or invalid credentials", "headers": { "WWW-Authenticate": { "schema": { "type": "string" }, "description": "Bearer resource_metadata=\"https://api.inboxguard.io/.well-known/oauth-protected-resource\"" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Forbidden": { "description": "Authenticated but missing required scope or role", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "NotFound": { "description": "Resource not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "RateLimited": { "description": "Rate limit exceeded", "headers": { "Retry-After": { "$ref": "#/components/headers/RetryAfter" }, "RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" }, "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" }, "RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "ServiceUnavailable": { "description": "Scanning temporarily disabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "Structured error envelope returned by every non-2xx response.",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": { "type": "string", "description": "Stable machine-readable code.", "enum": ["BAD_REQUEST", "UNAUTHORIZED", "FORBIDDEN", "NOT_FOUND", "CONFLICT", "RATE_LIMIT", "UPSTREAM_ERROR", "INTERNAL_ERROR"] },
              "message": { "type": "string", "description": "Human-readable description." },
              "requestId": { "type": "string", "description": "Correlation id for support." },
              "retryAfterSeconds": { "type": "integer", "description": "Present on RATE_LIMIT responses." }
            },
            "required": ["code", "message"]
          }
        },
        "required": ["error"]
      },
      "OkResult": { "type": "object", "properties": { "ok": { "type": "boolean" } }, "required": ["ok"] },
      "Pagination": { "type": "object", "properties": { "next_cursor": { "type": ["string", "null"], "description": "Cursor for the next page, or null when complete." } } },
      "Health": { "type": "object", "properties": { "status": { "type": "string", "enum": ["ok", "degraded"] }, "db": { "type": "string", "enum": ["ok", "error"] }, "durationMs": { "type": "integer" }, "version": { "type": "string" } }, "required": ["status", "db"] },
      "ScanRequest": {
        "type": "object",
        "properties": {
          "domain": { "type": "string", "minLength": 1, "maxLength": 253, "description": "The domain to scan, e.g. example.com.", "examples": ["example.com"] },
          "dkimSelectors": { "type": "array", "items": { "type": "string", "maxLength": 64 }, "maxItems": 20, "description": "Optional DKIM selectors to probe." },
          "turnstileToken": { "type": "string", "description": "Cloudflare Turnstile token for anonymous browser scans; ignored when authenticated." }
        },
        "required": ["domain"]
      },
      "ScanScore": {
        "type": "object",
        "description": "Overall deliverability score with a per-check point breakdown.",
        "properties": {
          "total": { "type": "integer", "minimum": 0, "maximum": 100, "description": "Overall 0–100 score." },
          "grade": { "type": "string", "enum": ["A", "B", "C", "D", "F"] },
          "breakdown": { "type": "object", "additionalProperties": { "type": "number" }, "description": "Points contributed by each check (spf, dmarc, dkim, …)." }
        },
        "required": ["total"]
      },
      "DomainCheck": {
        "type": "object",
        "description": "Result of one authentication/transport/reputation check. Carries `healthy` and `issues` plus check-specific fields (e.g. SPF `lookupCount`, DMARC `policy`, BIMI `vmcReachable`).",
        "properties": {
          "healthy": { "type": "boolean", "description": "Whether this check passed cleanly." },
          "issues": { "type": "array", "items": { "type": "string" }, "description": "Human-readable problems found." }
        },
        "additionalProperties": true
      },
      "ScanResult": {
        "type": "object",
        "properties": {
          "domain": { "type": "string" },
          "score": { "$ref": "#/components/schemas/ScanScore" },
          "spf": { "$ref": "#/components/schemas/DomainCheck" },
          "dmarc": { "$ref": "#/components/schemas/DomainCheck" },
          "ptr": { "$ref": "#/components/schemas/DomainCheck" },
          "dkim": { "$ref": "#/components/schemas/DomainCheck" },
          "mtaSts": { "$ref": "#/components/schemas/DomainCheck" },
          "tlsRpt": { "$ref": "#/components/schemas/DomainCheck" },
          "mxTls": { "$ref": "#/components/schemas/DomainCheck" },
          "bimi": { "$ref": "#/components/schemas/DomainCheck" },
          "blocklist": { "$ref": "#/components/schemas/DomainCheck" },
          "durationMs": { "type": "integer" },
          "plan": {
            "type": "object",
            "properties": {
              "tier": { "type": "string", "description": "public, free, entry, pro, or agency." },
              "trialing": { "type": "boolean" },
              "blocklistsChecked": { "description": "Number of blocklists checked, or 'all'.", "anyOf": [{ "type": "integer" }, { "type": "string" }] }
            }
          }
        },
        "required": ["domain", "score"]
      },
      "ScanList": { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/ScanResult" } }, "next_cursor": { "type": ["string", "null"] } }, "required": ["items"] },
      "Domain": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "domain": { "type": "string" },
          "score": { "type": "integer", "minimum": 0, "maximum": 100 },
          "lastScannedAt": { "type": "string", "format": "date-time" },
          "monitoringEnabled": { "type": "boolean" }
        },
        "required": ["id", "domain"]
      },
      "DomainList": { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/Domain" } }, "next_cursor": { "type": ["string", "null"] } }, "required": ["items"] },
      "DmarcReport": {
        "type": "object",
        "properties": {
          "domain": { "type": "string" },
          "reportId": { "type": "string" },
          "orgName": { "type": "string", "description": "Reporting receiver (e.g. google.com)." },
          "dateRangeBegin": { "type": "string", "format": "date-time" },
          "dateRangeEnd": { "type": "string", "format": "date-time" },
          "sourceIp": { "type": "string" },
          "count": { "type": "integer" },
          "disposition": { "type": "string", "enum": ["none", "quarantine", "reject"] },
          "dkim": { "type": "string", "enum": ["pass", "fail"] },
          "spf": { "type": "string", "enum": ["pass", "fail"] }
        },
        "required": ["domain", "sourceIp", "count"]
      },
      "DmarcReportList": { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/DmarcReport" } }, "next_cursor": { "type": ["string", "null"] } }, "required": ["items"] },
      "Alert": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "domain": { "type": "string" },
          "type": { "type": "string", "description": "Alert category, e.g. spf_broken, dmarc_weakened, blocklist_hit." },
          "severity": { "type": "string", "enum": ["info", "warning", "critical"] },
          "status": { "type": "string", "enum": ["open", "resolved"] },
          "message": { "type": "string" },
          "createdAt": { "type": "string", "format": "date-time" }
        },
        "required": ["id", "domain", "type", "status"]
      },
      "AlertList": { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/Alert" } }, "next_cursor": { "type": ["string", "null"] } }, "required": ["items"] },
      "ApiKey": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "name": { "type": "string" },
          "keyPrefix": { "type": "string", "description": "First 12 chars of the key for identification." },
          "environment": { "type": "string", "enum": ["live", "test"] },
          "scopes": { "type": "array", "items": { "type": "string", "enum": ["read", "full"] } },
          "lastUsedAt": { "type": ["string", "null"], "format": "date-time" },
          "expiresAt": { "type": ["string", "null"], "format": "date-time" },
          "createdAt": { "type": "string", "format": "date-time" }
        },
        "required": ["id", "name", "scopes"]
      },
      "ApiKeyList": { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/ApiKey" } }, "next_cursor": { "type": ["string", "null"] } }, "required": ["items"] },
      "CreateApiKeyRequest": {
        "type": "object",
        "properties": {
          "name": { "type": "string", "minLength": 1, "maxLength": 120 },
          "environment": { "type": "string", "enum": ["live", "test"], "default": "live" },
          "scopes": { "type": "array", "items": { "type": "string", "enum": ["read", "full"] }, "minItems": 1, "default": ["full"] },
          "expiresAt": { "type": "string", "format": "date-time" }
        },
        "required": ["name"]
      },
      "ApiKeyWithSecret": { "allOf": [{ "$ref": "#/components/schemas/ApiKey" }, { "type": "object", "properties": { "token": { "type": "string", "description": "The secret key. Shown only once." } }, "required": ["token"] }] }
    }
  }
}
