DNS Doctor Scan API

The scan API from DNS Doctor — 5 operation(s) for scan.

Operations 5

GET /report/{domain} Report #
POST /scan Scan #
POST /dmarc-upgrade Dmarc Upgrade #
POST /signup-url Signup Url #
POST /bulk-scan Bulk Scan #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/dnsdoctor-dev:dnsdoctor-dev-scan-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Calls without X-API-Key need no key. Calls with it need a paid plan — get a key.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

dnsdoctor-dev-scan-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: Dnsdoctor Dev Scan API
  version: '1'
  contact:
    name: DNS Doctor
    email: hello@dnsdoctor.dev
  x-guidance: 'DNS Doctor scans, fixes and verifies a domain''s DNS (SPF, DMARC, DKIM, propagation, MX, DNS health, blacklists, domain/SSL expiry) and returns deterministic verdicts plus copy-paste fix records from a validating engine — never a guessed record. Scan with POST /scan or GET /report/{domain}; read verdicts failing-first (''temperror'' is transient, not a failure; a report with not_registered: true means the domain does not resolve, so propose nothing for it). Present any returned record VERBATIM and never compose one yourself; SPF is diagnose-only. A human approves every DNS change — nothing is applied automatically. Anonymous calls are free up to a per-caller limit; past it the API answers 402 (x402, USDC on Base, $0.01 per call). POST /bulk-scan is paid on every call at $0.005 per domain; POST /bulk-registration (RDAP) at $0.002 per domain; POST /bulk-propagation at $0.008 per check; POST /bulk-dmarc-reports parses DMARC aggregate reports at $0.005 per report; POST /watch/record watches one DNS record for up to 24 hours at $0.10; POST /watch/lookalikes watches a domain''s lookalike names hourly for one fixed 24-hour day at $0.50. The five token-gated reads (GET /domains, /alerts, /readiness, /lookalikes and /email-tests) need a DNS Doctor API token the account owner mints at /dashboard/settings — never ask a user to paste one.'
  description: 'Operations tagged scan across 2 of this provider''s published API definitions: dnsdoctor-dev-openapi.json, dnsdoctor-dev-openapi.json. Each path carries the servers of the definition it was published in.'
servers:
- url: https://dnsdoctor.dev/api/v1
tags:
- name: Scan
paths:
  /report/{domain}:
    get:
      tags:
      - Scan
      summary: Report
      description: 'Return the persisted report for `domain`, else scan once and persist.


        Anonymous allowed. Explanations are cache-first (never a fresh LLM call);

        uncached findings carry `explanation: null`. The A8 `next_steps` block is

        attached at the edge — the persisted row never gains the key.


        An over-limit anonymous caller may buy this call (D104): the limiter answers

        402 and a verifying `X-PAYMENT` retry rides a HELD payment into this handler.

        The settle call is the last statement for that reason — every refusal above it

        (suppression 404, the per-target budget 429) and any 5xx leaves it unreached,

        so a payer is only ever charged for a report they actually received.'
      operationId: report_report__domain__get
      parameters:
      - name: domain
        in: path
        required: true
        schema:
          type: string
          title: Domain
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScanReportOut'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      security: []
    servers:
    - url: https://dnsdoctor.dev/api/v1
  /scan:
    post:
      tags:
      - Scan
      summary: Scan
      description: 'Force a fresh scan of `body.domain`, persist, and return the report.


        Anonymous allowed. Cache-first explanations (no fresh LLM call) and the A8

        `next_steps` block, same as `GET /v1/report`. "Force" is bounded by the same

        `scan_dedup_window_minutes` reuse the web `POST /scan` applies — see

        `api/pipeline.py::run_scan_pipeline` for why that bound belongs on this path

        too.'
      operationId: scan_scan_post
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ScanIn'
        required: true
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScanReportOut'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      security: []
    servers:
    - url: https://dnsdoctor.dev/api/v1
  /dmarc-upgrade:
    post:
      tags:
      - Scan
      summary: Dmarc Upgrade
      description: 'Return the server-gated DMARC enforcement record for `body.domain`.


        The REST twin of the `build_dmarc_upgrade` MCP tool, running the SAME core

        (`mcp_server/tools.py::build_dmarc_upgrade_from_domain`) rather than a second

        implementation, so the two agent transports cannot drift: the record is

        `build_upgrade_record` output byte for byte, and the ceiling is

        `p=quarantine`, unlocked by a **server-side** alignment gate — a caller

        cannot assert alignment to buy a stronger policy, and `p=reject` is not

        reachable from any scan (D101: only the readiness engine''s aggregate-report

        evidence unlocks it).


        Anonymous allowed. Scans fresh (see the core''s docstring for why a stale

        baseline is silently destructive here), which is why this path carries the

        same `enforce_v1_rate_limit` and per-target scan budget the scan routes do.


        `record: null` is an ANSWER, not an error: the domain does not exist, DMARC

        could not be read, the published policy is already at least as strong as

        this scan justifies, or the scan observed no alignment signal at all — the

        D101 reporting-first refusal, whose `rationale` asks for `rua=` reporting

        rather than offering a weaker record. Relay `rationale`; never compose a

        record to fill it.


        Error mapping — the transport-neutral core errors become HTTP:

        `ToolInputError` → 422 with the core''s own detail, `ToolTransientError` →

        503 (retrying the identical call is the right response), and the pipeline''s

        per-target budget → the same 429 both scan routes answer with. Suppression is

        refused here first, with the byte-identical 404 an unscanned domain gets, so

        the core''s own suppression branch (a `ToolInputError`, which would surface as

        a 422) is never reached.'
      operationId: dmarc_upgrade_dmarc_upgrade_post
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DmarcUpgradeIn'
        required: true
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DmarcUpgradeOut'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      security: []
    servers:
    - url: https://dnsdoctor.dev/api/v1
  /signup-url:
    post:
      tags:
      - Scan
      summary: Signup Url
      description: 'Return the monitoring signup hand-off for `body.domain`.


        The REST twin of the `start_monitoring_signup` MCP tool, running the SAME pure

        core (`mcp_server/signup.py::build_signup_url`). It performs **no I/O of any

        kind** — no DNS, no store read, no mail: it canonicalizes the domain and

        assembles two URLs rooted at `settings.public_base_url`. Nothing is created

        until the human opens `signup_url` and signs in there themselves.


        **No suppression check, deliberately** — the same decision `signup.py`

        documents for the tool, not an oversight. The valve is a store read, and the

        no-I/O property is what makes this surface''s closed-world claim true; nothing

        leaks by skipping it, because suppression withholds a *report* and both

        returned URLs still refuse on their own (`/scan/{domain}` is served by the

        suppression-aware pipeline, and `/start` publishes nothing about the domain —

        it prefills a name the caller already had).


        Anonymous allowed. `ToolInputError` → 422 with the core''s own detail, matching

        the `normalize_domain` contract the other v1 routes answer with.'
      operationId: signup_url_signup_url_post
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SignupUrlIn'
        required: true
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SignupUrlOut'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      security: []
    servers:
    - url: https://dnsdoctor.dev/api/v1
  /bulk-scan:
    post:
      tags:
      - Scan
      summary: Bulk Scan
      description: 'Scan 2-50 domains in one paid call. Anonymous; **402 on every call**.


        Priced per domain, computed from the batch — the 402 quotes the price for

        THIS request. Body validation runs first, so a malformed batch is a 422 that

        costs nothing; an inert deployment (no x402 credentials) answers 503, because

        a surface that can only be sold cannot be given away.


        `_UNPAID_OFFER_LIMIT` bounds the two shapes the lane''s own budget never sees:

        a request with no payment header, and one whose body is refused before the

        lane opens. The lane meters only headers it had to verify, so both would

        otherwise be unmetered on the one route with no limiter dependency. The

        header-less half is spent in `bound_unpaid_request` — a route dependency, so

        a body pydantic refuses on its own is on the counted path too.'
      operationId: bulk_scan_bulk_scan_post
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BulkScanIn'
        required: true
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BulkScanOut'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
        '402':
          description: Payment required — retry with an x402 payment header.
      x-payment-info:
        payTo: '0x6fDc1f22520151DfBc6273d6C48E413D434C7916'
        price:
          mode: dynamic
          currency: USD
          min: '0.01'
          max: '0.25'
        protocols:
        - x402: {}
        network: base
    servers:
    - url: https://dnsdoctor.dev/api/v1
components:
  schemas:
    SenderRequirements:
      properties:
        note:
          type: string
          title: Note
        providers:
          items:
            $ref: '#/components/schemas/Provider'
          type: array
          title: Providers
        items:
          items:
            $ref: '#/components/schemas/RequirementItem'
          type: array
          title: Items
      type: object
      required:
      - note
      - providers
      - items
      title: SenderRequirements
    ScanReportOut:
      properties:
        domain:
          type: string
          title: Domain
        checks:
          items:
            $ref: '#/components/schemas/CheckResult'
          type: array
          title: Checks
        scanned_at:
          type: string
          format: date-time
          title: Scanned At
        explanations_pending:
          type: boolean
          title: Explanations Pending
          default: false
        dns_provider_key:
          anyOf:
          - type: string
          - type: 'null'
          title: Dns Provider Key
        not_registered:
          type: boolean
          title: Not Registered
          default: false
        agent_scanned:
          type: boolean
          title: Agent Scanned
          default: false
        registration:
          anyOf:
          - $ref: '#/components/schemas/Registration'
          - type: 'null'
        next_steps:
          anyOf:
          - $ref: '#/components/schemas/NextSteps'
          - type: 'null'
        sender_requirements:
          anyOf:
          - $ref: '#/components/schemas/SenderRequirements'
          - type: 'null'
        indexable:
          type: boolean
          title: Indexable
          description: 'Thin-page shield: worth indexing only if mail-capable AND substantive.


            Mail-capable = any of mx/spf/dmarc observed a record; substantive = at

            least `_MIN_RESOLVED_CHECKS` checks resolved (non-TEMPERROR). NXDOMAIN,

            parked, and garbage domains fail the first gate; half-broken scans fail

            the second. Non-indexable reports stay fully viewable — they are only

            noindex + sitemap-excluded. Serializes into `report_json`; older

            persisted JSON without the key still validates (computed on read).'
          readOnly: true
        dns_provider:
          anyOf:
          - additionalProperties:
              anyOf:
              - type: string
              - type: 'null'
            type: object
          - type: 'null'
          title: Dns Provider
          description: 'The serialized assist-only NS provider, looked up by key at read time.


            Mirrors the domain-row `dns_provider` object: the key is stored, the full

            object (display name, console URL, guide anchor, collision note) is

            resolved from the `DNS_PROVIDERS` table on serialize, so a URL/copy fix

            never needs a re-scan. A key that no longer exists in the table (or None)

            serializes as None — the report page then omits the provider mention.'
          readOnly: true
      type: object
      required:
      - domain
      - checks
      - indexable
      - dns_provider
      title: ScanReportOut
      description: 'What a scan endpoint actually returns: the report plus the A8 hand-off.


        Declared as a response model rather than by widening `ScanReport` itself:

        `ScanReport` round-trips through the persisted `report_json`, and a field

        there would be frozen into stored rows. This subclass is never handed to

        `save_report`, so the non-persistence property is untouched while

        `/api/v1/openapi.json` documents the real response shape — which the agent

        channel (`/llms-full.txt`, U7''s api-catalog) depends on being accurate.'
    NextSteps:
      properties:
        summary:
          type: string
          title: Summary
        report_url:
          type: string
          title: Report Url
        monitor_url:
          type: string
          title: Monitor Url
        signup_url:
          anyOf:
          - type: string
          - type: 'null'
          title: Signup Url
        email_test_url:
          anyOf:
          - type: string
          - type: 'null'
          title: Email Test Url
        lookalikes:
          anyOf:
          - $ref: '#/components/schemas/LookalikesPointer'
          - type: 'null'
      type: object
      required:
      - summary
      - report_url
      - monitor_url
      title: NextSteps
      description: The hand-off block's wire shape (documentation only — see `build_next_steps`).
    ScanIn:
      properties:
        domain:
          type: string
          title: Domain
      type: object
      required:
      - domain
      title: ScanIn
      description: 'The `POST /v1/scan` body — a bare domain (no Turnstile: agents can''t solve one).'
    CheckStatus:
      type: string
      enum:
      - pass
      - warn
      - fail
      - info
      - temperror
      title: CheckStatus
    BulkScanOut:
      properties:
        domains_requested:
          type: integer
          title: Domains Requested
        domains_scanned:
          type: integer
          title: Domains Scanned
        results:
          items:
            $ref: '#/components/schemas/BulkScanResult'
          type: array
          title: Results
      type: object
      required:
      - domains_requested
      - domains_scanned
      - results
      title: BulkScanOut
      description: The batch response. `domains_scanned` is what the settle gate reads.
    HTTPValidationError:
      properties:
        detail:
          items:
            $ref: '#/components/schemas/ValidationError'
          type: array
          title: Detail
      type: object
      title: HTTPValidationError
    SignupUrlIn:
      properties:
        domain:
          type: string
          title: Domain
      type: object
      required:
      - domain
      title: SignupUrlIn
      description: The `POST /v1/signup-url` body — a bare domain, same shape as `ScanIn`.
    Registration:
      properties:
        registrar:
          anyOf:
          - type: string
          - type: 'null'
          title: Registrar
        registrar_iana_id:
          anyOf:
          - type: string
          - type: 'null'
          title: Registrar Iana Id
        registered_at:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          title: Registered At
        updated_at:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          title: Updated At
        expires_at:
          anyOf:
          - type: string
            format: date-time
          - type: 'null'
          title: Expires At
        status:
          items:
            type: string
          type: array
          title: Status
          default: []
        nameservers:
          items:
            type: string
          type: array
          title: Nameservers
          default: []
        dnssec:
          anyOf:
          - type: boolean
          - type: 'null'
          title: Dnssec
        abuse_email:
          anyOf:
          - type: string
          - type: 'null'
          title: Abuse Email
        redacted:
          type: boolean
          title: Redacted
          default: false
      type: object
      title: Registration
      description: 'What a registry publishes about one domain. Observation only.


        Every field is optional because every field is redactable: a registry that

        publishes only status codes and nameservers is the normal post-GDPR case,

        not a failure. `status` carries the registry''s own strings verbatim (RFC

        9083 spells them `client transfer prohibited`; some registries still emit

        EPP camelCase) — normalizing them would invent a vocabulary neither side

        uses.'
    BulkScanIn:
      properties:
        domains:
          items:
            type: string
            maxLength: 253
          type: array
          maxItems: 50
          title: Domains
      type: object
      required:
      - domains
      title: BulkScanIn
      description: 'The request body: 2-50 domains, canonicalized and deduped server-side.


        The upper bound is enforced TWICE, and both layers earn their place. At the

        model (below) so pydantic refuses an oversized array during parse; and again

        in `_normalize_batch`, which is what applies the bound to the DEDUPED count —

        the number actually scanned and priced.


        ⚠️ This docstring used to say the model-level bound was deliberately absent,

        on the reasoning that a pydantic-raised 422 skips the handler and so escapes

        metering. That reasoning was answered by `_metered_validation_handler`, which

        meters exactly those refusals, and the bound was added by the 2026-09-05

        review — a caller with a junk payment header could otherwise force a large

        allocation before anything counted it. Metering no longer depends on where

        the refusal is raised, so the bound belongs at the earliest layer.'
    ValidationError:
      properties:
        loc:
          items:
            anyOf:
            - type: string
            - type: integer
          type: array
          title: Location
        msg:
          type: string
          title: Message
        type:
          type: string
          title: Error Type
        input:
          title: Input
        ctx:
          type: object
          title: Context
      type: object
      required:
      - loc
      - msg
      - type
      title: ValidationError
    DmarcUpgradeOut:
      properties:
        domain:
          type: string
          title: Domain
        record:
          anyOf:
          - type: string
          - type: 'null'
          title: Record
        policy:
          anyOf:
          - type: string
          - type: 'null'
          title: Policy
        current_policy:
          anyOf:
          - type: string
          - type: 'null'
          title: Current Policy
        alignment_ok:
          type: boolean
          title: Alignment Ok
        rationale:
          type: string
          title: Rationale
        apply_note:
          type: string
          title: Apply Note
      type: object
      required:
      - domain
      - record
      - policy
      - current_policy
      - alignment_ok
      - rationale
      - apply_note
      title: DmarcUpgradeOut
      description: 'The `POST /v1/dmarc-upgrade` response — the MCP tool result, key for key.


        Declared explicitly (rather than left as a bare dict) so `/openapi.json`

        documents the real shape, including the nullability that carries the

        contract: `record` is null on every honest refusal — the answer is then

        `rationale`, and a caller must NEVER compose a record to fill the gap.

        `policy` names the policy the RETURNED record sets, so it is null exactly

        when `record` is; the domain''s observed policy travels in `current_policy`

        (also null when no DMARC record could be read).


        The field set mirrors `mcp_server/tools.py::build_dmarc_upgrade_tool` — the

        same core both surfaces run — so the REST body is byte-equal to the MCP tool

        result by construction. A key added there is added here.'
    BulkScanResult:
      properties:
        domain:
          type: string
          title: Domain
        status:
          type: string
          enum:
          - ok
          - error
          title: Status
        report:
          anyOf:
          - $ref: '#/components/schemas/ScanReportOut'
          - type: 'null'
        error:
          anyOf:
          - type: string
          - type: 'null'
          title: Error
      type: object
      required:
      - domain
      - status
      title: BulkScanResult
      description: 'One domain''s outcome. `report` and `error` are mutually exclusive.


        An entry is never silently dropped: a caller who paid for fifty names gets

        fifty entries, so "missing" can never be mistaken for "clean".'
    DmarcUpgradeIn:
      properties:
        domain:
          type: string
          title: Domain
      type: object
      required:
      - domain
      title: DmarcUpgradeIn
      description: The `POST /v1/dmarc-upgrade` body — a bare domain, same shape as `ScanIn`.
    Provider:
      properties:
        key:
          type: string
          title: Key
        name:
          type: string
          title: Name
        source_url:
          type: string
          title: Source Url
        bulk_threshold:
          type: string
          title: Bulk Threshold
      type: object
      required:
      - key
      - name
      - source_url
      - bulk_threshold
      title: Provider
    RequirementItem:
      properties:
        key:
          type: string
          title: Key
        label:
          type: string
          title: Label
        providers:
          additionalProperties:
            type: string
            enum:
            - all
            - bulk
          type: object
          title: Providers
        status:
          type: string
          enum:
          - pass
          - fail
          - unknown
          - email_test
          - not_visible
          title: Status
        evidence:
          anyOf:
          - type: string
          - type: 'null'
          title: Evidence
        provider_status:
          anyOf:
          - additionalProperties:
              type: string
              enum:
              - pass
              - fail
              - unknown
              - email_test
              - not_visible
            type: object
          - type: 'null'
          title: Provider Status
      type: object
      required:
      - key
      - label
      - providers
      - status
      title: RequirementItem
    CachedLookalikes:
      properties:
        summary:
          type: string
          title: Summary
        resolving:
          type: integer
          title: Resolving
        accepts_mail:
          type: integer
          title: Accepts Mail
        checked_at:
          type: string
          title: Checked At
      type: object
      required:
      - summary
      - resolving
      - accepts_mail
      - checked_at
      title: CachedLookalikes
      description: A still-fresh `check_lookalikes` result's counts — read from its cache, never probed.
    CheckResult:
      properties:
        check:
          type: string
          title: Check
        status:
          $ref: '#/components/schemas/CheckStatus'
        title:
          type: string
          title: Title
        raw:
          anyOf:
          - type: string
          - type: 'null'
          title: Raw
        explanation:
          anyOf:
          - type: string
          - type: 'null'
          title: Explanation
        fix_record:
          anyOf:
          - type: string
          - type: 'null'
          title: Fix Record
        details:
          anyOf:
          - items:
              type: string
            type: array
          - type: 'null'
          title: Details
        unknown:
          anyOf:
          - items:
              type: string
            type: array
          - type: 'null'
          title: Unknown
        withheld_code:
          anyOf:
          - type: string
            const: hosted
          - type: 'null'
          title: Withheld Code
      type: object
      required:
      - check
      - status
      - title
      title: CheckResult
      description: The outcome of one check (e.g. SPF, DMARC, expiry).
    SignupUrlOut:
      properties:
        signup_url:
          type: string
          title: Signup Url
        report_url:
          type: string
          title: Report Url
        message:
          type: string
          title: Message
        why_monitoring:
          type: string
          title: Why Monitoring
        guidance:
          additionalProperties: true
          type: object
          title: Guidance
      type: object
      required:
      - signup_url
      - report_url
      - message
      - why_monitoring
      - guidance
      title: SignupUrlOut
      description: 'The `POST /v1/signup-url` response — `build_signup_url`''s dict, key for key.


        All five fields are relayed VERBATIM and a caller must never rebuild any of

        them: the U5 verbatim-field rule is the whole point of returning both URLs

        (`mcp_server/signup.py`''s module docstring), `message` is fixed copy with

        exactly one substitution — a hand-off a human reads, not prose to paraphrase —

        and `why_monitoring` is fixed copy with none.


        Mirrors `mcp_server/signup.py::build_signup_url` so the REST body is byte-equal

        to the `start_monitoring_signup` MCP tool result by construction.'
    LookalikesPointer:
      properties:
        tool:
          type: string
          const: check_lookalikes
          title: Tool
          default: check_lookalikes
        url:
          type: string
          title: Url
        cached:
          anyOf:
          - $ref: '#/components/schemas/CachedLookalikes'
          - type: 'null'
      type: object
      required:
      - url
      title: LookalikesPointer
      description: 'D130: the scan''s pointer to the separate `check_lookalikes` call.


        Filled by `api/lookalike_pointer.py` on the AGENT surfaces only (MCP

        `scan_domain`/`get_report`, v1 `/report` + `/scan`). The web `GET /report`

        that feeds the indexable `/scan/[domain]` page never fills it (the D128

        no-teaser rule), so it serializes there as null.'
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: 'A DNS Doctor API token: `Authorization: Bearer dnsd_…`.'
x-refined-from:
- dnsdoctor-dev-openapi.json
- dnsdoctor-dev-openapi.json
x-discovery:
  ownershipProofs:
  - '0xeb49cc7d9ac04add7ee0fbe51bc806b684ebdb6bfcdc8c2d7aa7eebc02032b93403764b0986d036ed68ad7e5914edc5403209f19806f4a87e15b1a5bdb73e9ab1b'