openapi: 3.0.3
info:
  title: RankParse API
  version: 1.0.0
  description: >
    SEO data API for developers and AI agents, powered by pre-processed Common Crawl data
    served on Cloudflare's edge.


    **20 data endpoints across 5 categories:**

    - **Link graph** — backlinks, referring domains, outbound links, anchor text, top pages
    - **Domain metrics** — authority score (0-100), domain rank, crawl history
    - **Real-time page analysis** — full SEO audit, site health, sitemap discovery, tech stack, Core Web Vitals
    - **Cross-domain intelligence** — domain overlap, link intersect, competitor gap, similar domains
    - **Power endpoints** — link audit, site explorer, batch backlinks (up to 50 domains)


    Billed per call — no subscription, no monthly floor. Starting at $0.009/credit. Credits never expire.


    REST API and MCP server available. Base URL: `https://api.rankparse.com`


    This specification covers the billable data endpoints and the multi-engine rank
    tracking reads. Dashboard and account management, the OAuth integration callbacks
    (Search Console, Google Ads, Gmail, Bing, Yandex), the outreach and Zeekeo surfaces,
    and the free unauthenticated tools under `/v1/tools/` are intentionally out of scope.
  contact:
    name: RankParse Support
    url: https://rankparse.com
  license:
    name: Proprietary
    url: https://rankparse.com/terms

servers:
  - url: https://api.rankparse.com
    description: Production

security:
  - ApiKeyAuth: []

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key

  parameters:
    domain:
      name: domain
      in: query
      required: true
      schema:
        type: string
      description: The domain to query (e.g. "example.com")
    limit:
      name: limit
      in: query
      required: false
      schema:
        type: integer
        default: 100
        minimum: 1
        maximum: 1000
      description: Maximum number of results to return (default 100, max 1000)
    offset:
      name: offset
      in: query
      required: false
      schema:
        type: integer
        default: 0
        minimum: 0
      description: Number of results to skip for pagination
    score:
      name: score
      in: query
      required: false
      schema:
        type: boolean
        default: false
      description: Enable derived score enrichment on each row (adds extra credit cost)
    backlinksSort:
      name: sort
      in: query
      required: false
      schema:
        type: string
        enum: [importance, recent]
        default: importance
      description: >
        Backlink ranking mode. `importance` (default) returns aggregated referrer rows;
        `recent` returns freshest URL-level backlinks.
    backlinksFromDomain:
      name: from_domain
      in: query
      required: false
      schema:
        type: string
      description: Optional source-domain filter (exact domain).
    backlinksLinkType:
      name: link_type
      in: query
      required: false
      schema:
        type: string
      description: Optional link type filter, e.g. `hyperlink` or `image`.
    domainsCsv:
      name: domains
      in: query
      required: true
      schema:
        type: string
      description: Comma-separated domains. Minimum 2, maximum 5.
    rankTrackingSiteId:
      name: site_id
      in: query
      required: true
      schema:
        type: string
      description: Logical RankParse website ID, from `GET /v1/rank-tracking/sites`.
    rankTrackingKeywordIdOptional:
      name: keyword_id
      in: query
      required: false
      schema:
        type: string
      description: >
        Restrict to one tracked keyword ID, from `GET /v1/rank-tracking/keywords`.
        Omit to return every tracked keyword for the site.
    rankTrackingLimit:
      name: limit
      in: query
      required: false
      schema:
        type: integer
        default: 100
        minimum: 1
        maximum: 100
      description: Maximum rows to return (default 100, max 100).
    rankTrackingEngineAll:
      name: engine
      in: query
      required: false
      schema:
        type: string
        enum: [all, google, bing, yandex]
        default: all
      description: >
        Search engine to report on. Defaults to `all` — every engine enabled
        for this account (see `enabled_engines` in the response). Comparing
        engines side by side is the primary use case, so leave this unset
        unless you need one engine only.
    rankTrackingEngineRequired:
      name: engine
      in: query
      required: true
      schema:
        type: string
        enum: [google, bing, yandex]
      description: >
        Search engine to get history for. Required and never `all` — history
        is always single-engine, unlike rankings.
    rankTrackingPeriodDays:
      name: period_days
      in: query
      required: false
      schema:
        type: integer
        default: 28
        minimum: 1
        maximum: 90
      description: Length of the current/previous comparison window, in days (default 28, max 90).
    rankTrackingStartDate:
      name: start_date
      in: query
      required: true
      schema:
        type: string
        format: date
      description: Start of the history range, inclusive, as `YYYY-MM-DD`.
    rankTrackingEndDate:
      name: end_date
      in: query
      required: true
      schema:
        type: string
        format: date
      description: >
        End of the history range, inclusive, as `YYYY-MM-DD`. The inclusive
        span from `start_date` to `end_date` may not exceed 400 days.

  schemas:
    Error:
      type: object
      required: [error, code, message]
      properties:
        error:
          type: string
        code:
          type: string
        message:
          type: string

    EnvelopeFields:
      type: object
      required: [credits_used, credits_remaining]
      properties:
        domain:
          type: string
        url:
          type: string
        total:
          type: integer
        limit:
          type: integer
        offset:
          type: integer
        credits_used:
          type: integer
          minimum: 0
        credits_remaining:
          type: integer
          minimum: 0

    BacklinkRow:
      type: object
      required: [from_domain, from_url, to_url, link_type, crawled_at]
      properties:
        from_domain:
          type: string
        from_url:
          type: string
        to_url:
          type: string
        anchor_text:
          type: string
          nullable: true
        rel:
          type: string
          nullable: true
        link_type:
          type: string
        domain_host_count:
          type: number
          minimum: 0
          nullable: true
        crawled_at:
          type: string
        from_domain_score:
          type: integer
          minimum: 0
          maximum: 100
        anchor_type:
          type: string
          enum: [branded, partial, naked, generic, image]
        link_quality:
          type: string
          enum: [high, medium, low]

    ReferringDomainRow:
      type: object
      required: [from_domain, dofollow_links, nofollow_links, total_links]
      properties:
        from_domain:
          type: string
        dofollow_links:
          type: integer
          minimum: 0
          description: >
            Always 0. The Common Crawl domain graph this table is built from carries
            no rel attribute, so dofollow/nofollow cannot be distinguished.
        nofollow_links:
          type: integer
          minimum: 0
          description: >
            Always 0. See dofollow_links.
        total_links:
          type: integer
          minimum: 0
        from_domain_score:
          type: integer
          minimum: 0
          maximum: 100

    OutboundLinkRow:
      type: object
      required: [to_domain, to_url, link_type, edge_count]
      properties:
        to_domain:
          type: string
        to_url:
          type: string
        anchor_text:
          type: string
          nullable: true
        link_type:
          type: string
        edge_count:
          type: integer
          minimum: 1

    OutboundSummary:
      type: object
      required: [unique_destinations, top_domain, top_domain_concentration]
      properties:
        unique_destinations:
          type: integer
          minimum: 0
        top_domain:
          type: string
          nullable: true
        top_domain_concentration:
          type: number
          minimum: 0
          maximum: 1

    AnchorTextRow:
      type: object
      required: [anchor_text, link_count, domain_count]
      properties:
        anchor_text:
          type: string
        link_count:
          type: integer
          minimum: 1
        domain_count:
          type: integer
          minimum: 1

    TopPageRow:
      type: object
      required: [url, inbound_links, referring_domains]
      properties:
        url:
          type: string
        inbound_links:
          type: integer
          minimum: 1
        referring_domains:
          type: integer
          minimum: 1
        status_code:
          type: integer
          nullable: true
          description: Always null. Not populated by the v1 pipeline.
        mime:
          type: string
          nullable: true
          description: Always null. Not populated by the v1 pipeline.

    DomainAuthority:
      type: object
      required: [score, referring_domains, total_host_count, registered_at, registrar, popularity_rank, health_score, risk_flags]
      properties:
        score:
          type: integer
          minimum: 0
          maximum: 100
        referring_domains:
          type: integer
          minimum: 0
        total_host_count:
          type: number
          minimum: 0
        registered_at:
          type: string
          nullable: true
        registrar:
          type: string
          nullable: true
        popularity_rank:
          type: integer
          minimum: 1
          nullable: true
        health_score:
          type: integer
          minimum: 0
          maximum: 100
        risk_flags:
          type: array
          items:
            type: string
            enum: [new_domain, high_authority_new_domain, no_tranco_presence, low_authority]

    DomainRank:
      type: object
      required: [inbound_edges, unique_domains, avg_linking_host_count]
      properties:
        inbound_edges:
          type: integer
          minimum: 0
        unique_domains:
          type: integer
          minimum: 0
        avg_linking_host_count:
          type: number
          minimum: 0

    CrawlHistory:
      type: object
      required: [total_urls]
      properties:
        first_seen:
          type: string
          nullable: true
        last_seen:
          type: string
          nullable: true
        total_urls:
          type: integer
          minimum: 0

    TechStack:
      type: object
      properties:
        server:
          type: string
          nullable: true
        x_powered_by:
          type: string
          nullable: true
        generator:
          type: string
          nullable: true

    PageSeoTitle:
      type: object
      properties:
        text:
          type: string
          nullable: true
        length:
          type: integer
        optimal:
          type: boolean

    PageSeoDescription:
      type: object
      properties:
        text:
          type: string
          nullable: true
        length:
          type: integer
        optimal:
          type: boolean

    PageSeoCanonical:
      type: object
      properties:
        url:
          type: string
          nullable: true
        self_referential:
          type: boolean

    PageSeoRobots:
      type: object
      properties:
        index:
          type: boolean
        follow:
          type: boolean
        raw:
          type: string
          nullable: true

    PageSeoLanguage:
      type: object
      properties:
        primary:
          type: string
          nullable: true
        tag:
          type: string
          nullable: true
        source:
          type: string
          nullable: true

    PageSeoHreflang:
      type: object
      properties:
        lang:
          type: string
        url:
          type: string

    PageSeoHeadings:
      type: object
      properties:
        h1:
          type: array
          items:
            type: string
        h2:
          type: array
          items:
            type: string
        h3_count:
          type: integer

    PageSeoOg:
      type: object
      properties:
        title:
          type: string
          nullable: true
        description:
          type: string
          nullable: true
        image:
          type: string
          nullable: true
        type:
          type: string
          nullable: true
        url:
          type: string
          nullable: true

    PageSeoTwitter:
      type: object
      properties:
        card:
          type: string
          nullable: true
        title:
          type: string
          nullable: true

    PageSeoImages:
      type: object
      properties:
        total:
          type: integer
        missing_alt:
          type: integer

    PageSeoLinks:
      type: object
      properties:
        internal:
          type: integer
        external:
          type: integer

    PageSeoData:
      type: object
      required: [url, status_code]
      properties:
        url:
          type: string
        final_url:
          type: string
        status_code:
          type: integer
        response_time_ms:
          type: integer
        title:
          $ref: '#/components/schemas/PageSeoTitle'
        description:
          $ref: '#/components/schemas/PageSeoDescription'
        canonical:
          $ref: '#/components/schemas/PageSeoCanonical'
        robots:
          $ref: '#/components/schemas/PageSeoRobots'
        viewport:
          type: string
          nullable: true
        language:
          $ref: '#/components/schemas/PageSeoLanguage'
        hreflang:
          type: array
          items:
            $ref: '#/components/schemas/PageSeoHreflang'
        headings:
          $ref: '#/components/schemas/PageSeoHeadings'
        og:
          $ref: '#/components/schemas/PageSeoOg'
        twitter:
          $ref: '#/components/schemas/PageSeoTwitter'
        json_ld:
          type: array
          items:
            type: object
        images:
          $ref: '#/components/schemas/PageSeoImages'
        links:
          $ref: '#/components/schemas/PageSeoLinks'
        word_count:
          type: integer
        favicon:
          type: string
          nullable: true

    SiteHealthHttps:
      type: object
      properties:
        enforced:
          type: boolean
        hsts:
          type: boolean
        hsts_max_age:
          type: integer
          nullable: true

    SiteHealthWwwRedirect:
      type: object
      properties:
        enabled:
          type: boolean
        target:
          type: string
          nullable: true

    SiteHealthCheck:
      type: object
      properties:
        url:
          type: string
        status:
          type: integer
          nullable: true
        content_type:
          type: string
          nullable: true
        response_time_ms:
          type: integer
          nullable: true

    SiteHealthSecurityHeaders:
      type: object
      properties:
        x_frame_options:
          type: string
          nullable: true
        x_content_type_options:
          type: string
          nullable: true
        content_security_policy:
          type: boolean
        strict_transport_security:
          type: string
          nullable: true
        referrer_policy:
          type: string
          nullable: true

    SiteHealthRobotsTxt:
      type: object
      properties:
        present:
          type: boolean
        disallow_all:
          type: boolean
        sitemap_declared:
          type: boolean

    SiteHealthData:
      type: object
      required: [domain, https, www_redirect, checks, security_headers, robots_txt]
      properties:
        domain:
          type: string
        https:
          $ref: '#/components/schemas/SiteHealthHttps'
        www_redirect:
          $ref: '#/components/schemas/SiteHealthWwwRedirect'
        checks:
          type: array
          items:
            $ref: '#/components/schemas/SiteHealthCheck'
        security_headers:
          $ref: '#/components/schemas/SiteHealthSecurityHeaders'
        robots_txt:
          $ref: '#/components/schemas/SiteHealthRobotsTxt'

    SitemapEntry:
      type: object
      required: [url]
      properties:
        url:
          type: string
        lastmod:
          type: string
          nullable: true
        changefreq:
          type: string
          nullable: true
        priority:
          type: string
          nullable: true

    DomainOverlapRow:
      type: object
      required: [from_domain, total_links, targets_linked]
      properties:
        from_domain:
          type: string
        total_links:
          type: number
          minimum: 0
        targets_linked:
          type: integer
          minimum: 1

    LinkIntersectRow:
      type: object
      required: [from_domain, total_links]
      properties:
        from_domain:
          type: string
        total_links:
          type: number
          minimum: 0

    SimilarDomainRow:
      type: object
      required: [similar_domain, shared_linkers]
      properties:
        similar_domain:
          type: string
        shared_linkers:
          type: integer
          minimum: 1

    CompetitorGapRow:
      type: object
      required: [from_domain, total_links, from_domain_score]
      properties:
        from_domain:
          type: string
        total_links:
          type: number
          minimum: 0
        from_domain_score:
          type: integer
          minimum: 0
          maximum: 100

    LinkAuditQuality:
      type: object
      required: [high, medium, low, total]
      properties:
        high:
          type: integer
          minimum: 0
        medium:
          type: integer
          minimum: 0
        low:
          type: integer
          minimum: 0
        total:
          type: integer
          minimum: 0

    LinkAuditBacklink:
      type: object
      required: [from_domain, from_domain_score, anchor_type, link_quality]
      properties:
        from_domain:
          type: string
        from_domain_score:
          type: integer
          minimum: 0
          maximum: 100
        anchor_type:
          type: string
          enum: [branded, partial, naked, generic, image]
        link_quality:
          type: string
          enum: [high, medium, low]

    LinkAudit:
      type: object
      required:
        - domain
        - health_score
        - risk_flags
        - link_quality
        - anchor_profile
        - top_backlinks
        - risk_backlinks
        - outbound_summary
        - credits_used
        - credits_remaining
      properties:
        domain:
          type: string
        health_score:
          type: integer
          minimum: 0
          maximum: 100
        risk_flags:
          type: array
          items:
            type: string
            enum: [new_domain, high_authority_new_domain, no_tranco_presence, low_authority]
        link_quality:
          $ref: '#/components/schemas/LinkAuditQuality'
        anchor_profile:
          type: object
          required: [branded, partial, generic, naked, image]
          properties:
            branded:
              type: number
              minimum: 0
              maximum: 1
            partial:
              type: number
              minimum: 0
              maximum: 1
            generic:
              type: number
              minimum: 0
              maximum: 1
            naked:
              type: number
              minimum: 0
              maximum: 1
            image:
              type: number
              minimum: 0
              maximum: 1
        top_backlinks:
          type: array
          items:
            $ref: '#/components/schemas/LinkAuditBacklink'
        risk_backlinks:
          type: array
          items:
            $ref: '#/components/schemas/LinkAuditBacklink'
        outbound_summary:
          $ref: '#/components/schemas/OutboundSummary'
        credits_used:
          type: integer
          minimum: 0
        credits_remaining:
          type: integer
          minimum: 0
        crawl_release:
          type: string
        cached:
          type: boolean

    SiteExplorer:
      type: object
      required: [backlinks, backlinks_total, authority, top_pages, anchor_text]
      properties:
        backlinks:
          type: array
          items:
            $ref: '#/components/schemas/BacklinkRow'
        backlinks_total:
          type: integer
          minimum: 0
        authority:
          $ref: '#/components/schemas/DomainAuthority'
        top_pages:
          type: array
          items:
            $ref: '#/components/schemas/TopPageRow'
        anchor_text:
          type: array
          items:
            $ref: '#/components/schemas/AnchorTextRow'

    BatchResultItem:
      type: object
      required: [domain, success]
      properties:
        domain:
          type: string
        data:
          type: array
          items:
            $ref: '#/components/schemas/BacklinkRow'
        total:
          type: integer
        success:
          type: boolean
        error:
          type: string

    PagePerformanceCategory:
      type: object
      properties:
        score:
          type: number
          minimum: 0
          maximum: 1
          nullable: true
        display_value:
          type: string

    PagePerformanceMetric:
      type: object
      properties:
        value:
          type: number
        display_value:
          type: string
        score:
          type: number
          nullable: true

    PagePerformanceData:
      type: object
      properties:
        url:
          type: string
        strategy:
          type: string
          enum: [mobile, desktop]
        performance_score:
          type: number
          minimum: 0
          maximum: 1
          nullable: true
        accessibility_score:
          type: number
          minimum: 0
          maximum: 1
          nullable: true
        seo_score:
          type: number
          minimum: 0
          maximum: 1
          nullable: true
        best_practices_score:
          type: number
          minimum: 0
          maximum: 1
          nullable: true
        metrics:
          type: object
          properties:
            first_contentful_paint:
              $ref: '#/components/schemas/PagePerformanceMetric'
            largest_contentful_paint:
              $ref: '#/components/schemas/PagePerformanceMetric'
            total_blocking_time:
              $ref: '#/components/schemas/PagePerformanceMetric'
            cumulative_layout_shift:
              $ref: '#/components/schemas/PagePerformanceMetric'
            speed_index:
              $ref: '#/components/schemas/PagePerformanceMetric'
            time_to_interactive:
              $ref: '#/components/schemas/PagePerformanceMetric'

    StubData:
      type: object
      required: [status, reason]
      properties:
        status:
          type: string
          enum: ['not_yet_available']
        reason:
          type: string

    SchemaMarkupEntity:
      type: object
      required: [type, id, block_index, source, properties]
      properties:
        type:
          type: array
          description: Normalized schema.org @type(s) — always an array, even for a single string @type.
          items:
            type: string
        id:
          type: string
          nullable: true
          description: The entity's @id, if present.
        block_index:
          type: integer
          description: Index (0-based) of the <script type="application/ld+json"> block this entity came from.
        source:
          type: string
          enum: [top_level, graph]
          description: Whether this entity was the block's top-level object/array item, or nested inside @graph.
        properties:
          type: object
          description: The full parsed JSON-LD object for this entity, including @context/@type/@id and all fields.

    SchemaMarkupInvalidBlock:
      type: object
      required: [index, error]
      properties:
        index:
          type: integer
          description: Index (0-based) of the malformed <script type="application/ld+json"> block.
        error:
          type: string
          description: The JSON parse error message.

    LayerStatus:
      type: object
      required: [ok]
      properties:
        ok:
          type: boolean
        latency_ms:
          type: number
        detail:
          type: string
        error:
          type: string

    HealthResponse:
      type: object
      required: [overall, timestamp, crawl_release, d1, r2, r2sql]
      properties:
        overall:
          type: string
          enum: ['healthy', 'degraded', 'down']
        timestamp:
          type: string
        crawl_release:
          type: string
        d1:
          $ref: '#/components/schemas/LayerStatus'
        r2:
          $ref: '#/components/schemas/LayerStatus'
        r2sql:
          $ref: '#/components/schemas/LayerStatus'

    RankTrackingEngine:
      type: string
      enum: [google, bing, yandex]
      description: A connected search engine provider.

    RankTrackingAvailability:
      type: string
      enum:
        - available
        - pending_first_sync
        - provider_limited
        - not_supported
        - authorization_expired
        - stale
        - sync_failed
        - no_observed_data
      description: >
        Data-quality state for one engine on one keyword/day. `available` means
        the metrics reflect a normal synced observation. Any other value means
        `position`/`clicks`/`impressions` may be null even though the engine is
        connected and attached — it is a data-quality or sync-timing signal,
        never evidence the keyword ranks nowhere. `provider_limited` is common
        for Bing and Yandex, whose APIs return less per-query data than
        Google's.

    RankTrackingSiteProvider:
      type: object
      required: [engine, property_id, property_url, is_active, last_synced_at, data_as_of, latest_sync_status, latest_sync_error_code]
      properties:
        engine:
          $ref: '#/components/schemas/RankTrackingEngine'
        property_id:
          type: string
        property_url:
          type: string
        is_active:
          type: boolean
        last_synced_at:
          type: integer
          nullable: true
          description: Unix epoch milliseconds of the last completed sync run for this property, or null if it has never synced.
        data_as_of:
          type: string
          nullable: true
          description: Latest `YYYY-MM-DD` date with synced data for this engine/property, or null.
        latest_sync_status:
          type: string
          nullable: true
        latest_sync_error_code:
          type: string
          nullable: true

    RankTrackingSite:
      type: object
      required: [site_id, canonical_host, display_name, providers]
      properties:
        site_id:
          type: string
        canonical_host:
          type: string
        display_name:
          type: string
        providers:
          type: object
          description: >
            Keyed by engine name (`google`, `bing`, `yandex`). Only engines
            with a primary property attached to this site are present — a
            missing key means that engine is not connected/attached here, not
            that it ranked nowhere.
          additionalProperties:
            $ref: '#/components/schemas/RankTrackingSiteProvider'

    TrackedKeyword:
      type: object
      required: [id, query, created_at]
      properties:
        id:
          type: string
        query:
          type: string
        created_at:
          type: integer
          description: Unix epoch milliseconds.

    RankTrackingPeriod:
      type: object
      required: [start_date, end_date]
      properties:
        start_date:
          type: string
        end_date:
          type: string

    RankTrackingMetrics:
      type: object
      required: [clicks, impressions, ctr, position, click_position]
      properties:
        clicks:
          type: integer
          nullable: true
        impressions:
          type: integer
          nullable: true
        ctr:
          type: number
          nullable: true
        position:
          type: number
          nullable: true
          description: >
            Provider-reported average position over the period. This is a
            provider-side aggregate, not a live neutral-location SERP rank
            check. Null does not necessarily mean "not ranking" — check the
            sibling `availability` field on the enclosing object first.
        click_position:
          type: number
          nullable: true
          description: Click-weighted average position, where the provider supports it; otherwise null.

    RankTrackingEngineSummary:
      type: object
      required: [engine, property_id, property_url, availability, data_as_of, top_page, current_period, previous_period, current, previous, movement]
      properties:
        engine:
          $ref: '#/components/schemas/RankTrackingEngine'
        property_id:
          type: string
        property_url:
          type: string
        availability:
          $ref: '#/components/schemas/RankTrackingAvailability'
        data_as_of:
          type: string
          nullable: true
        top_page:
          type: string
          nullable: true
        current_period:
          $ref: '#/components/schemas/RankTrackingPeriod'
        previous_period:
          $ref: '#/components/schemas/RankTrackingPeriod'
        current:
          $ref: '#/components/schemas/RankTrackingMetrics'
        previous:
          $ref: '#/components/schemas/RankTrackingMetrics'
        movement:
          type: number
          nullable: true
          description: >
            `previous.position` minus `current.position`. Positive means the
            keyword moved up (better rank). Null unless both periods have a
            non-null `position`.

    RankTrackingKeywordSummary:
      type: object
      required: [id, query, engines]
      properties:
        id:
          type: string
        query:
          type: string
        engines:
          type: object
          description: >
            Keyed by engine name (`google`, `bing`, `yandex`). A missing key
            means that engine is not connected/attached to this site (compare
            against the response's top-level `enabled_engines` to tell
            "disabled for my account" apart from "enabled but not attached
            here") — never a zero or bottom rank.
          additionalProperties:
            $ref: '#/components/schemas/RankTrackingEngineSummary'

    RankTrackingHistoryRow:
      type: object
      required: [date, clicks, impressions, ctr, position, click_position]
      properties:
        date:
          type: string
        clicks:
          type: integer
          nullable: true
        impressions:
          type: integer
          nullable: true
        ctr:
          type: number
          nullable: true
        position:
          type: number
          nullable: true
        click_position:
          type: number
          nullable: true

  responses:
    BadRequest:
      description: Invalid request parameters
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    PaymentRequired:
      description: Insufficient credits
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    RateLimited:
      description: Rate limit exceeded (60 requests/minute)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

paths:
  /ping:
    get:
      operationId: getPing
      summary: Ping
      description: Fast liveness check. Returns immediately with no database queries. No authentication required.
      security: []
      tags: [System]
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [ok]
                properties:
                  ok:
                    type: boolean

  /health:
    get:
      operationId: getHealth
      summary: Health check
      description: Returns the health status of all subsystems (D1, R2, R2 SQL). No authentication required.
      security: []
      tags: [System]
      responses:
        '200':
          description: All systems healthy
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'
        '207':
          description: Degraded — some subsystems unhealthy
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'
        '503':
          description: Down — critical subsystems unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'

  /v1/credits:
    get:
      operationId: getCredits
      summary: Check credit balance
      description: Returns the current credit balance for the authenticated user. Does not deduct credits.
      tags: [Account]
      responses:
        '200':
          description: Current credit balance
          content:
            application/json:
              schema:
                type: object
                required: [credits]
                properties:
                  credits:
                    type: integer
                    minimum: 0
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/backlinks:
    get:
      operationId: getBacklinks
      summary: Get backlinks for a domain
      description: >
        Returns backlinks pointing to the specified domain from Common Crawl data.
        Default `sort=importance` returns aggregated referrer rows ordered by domain authority.
        `sort=recent` returns URL-level backlink rows ordered by crawl recency.
        Costs 2 credits (or 3 credits with `score=true`).
      tags: [Link Graph]
      parameters:
        - $ref: '#/components/parameters/domain'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/score'
        - $ref: '#/components/parameters/backlinksSort'
        - $ref: '#/components/parameters/backlinksFromDomain'
        - $ref: '#/components/parameters/backlinksLinkType'
      responses:
        '200':
          description: Backlinks for the domain
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/EnvelopeFields'
                  - type: object
                    required: [data]
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/BacklinkRow'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/referring-domains:
    get:
      operationId: getReferringDomains
      summary: Get referring domains
      description: Returns domains that link to the specified domain with dofollow/nofollow link counts. Costs 2 credits (or 3 credits with score=true).
      tags: [Link Graph]
      parameters:
        - $ref: '#/components/parameters/domain'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/score'
      responses:
        '200':
          description: Referring domains
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/EnvelopeFields'
                  - type: object
                    required: [data]
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/ReferringDomainRow'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/outbound-links:
    get:
      operationId: getOutboundLinks
      summary: Get outbound links from a domain
      description: Returns external links going out from the specified domain with anchor text and link type. Costs 2 credits.
      tags: [Link Graph]
      parameters:
        - $ref: '#/components/parameters/domain'
        - $ref: '#/components/parameters/limit'
      responses:
        '200':
          description: Outbound links
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/EnvelopeFields'
                  - type: object
                    required: [data]
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/OutboundLinkRow'
                      summary:
                        $ref: '#/components/schemas/OutboundSummary'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/anchor-text:
    get:
      operationId: getAnchorText
      summary: Get anchor text distribution
      description: Returns anchor text phrases used in links pointing to the domain, with link and domain counts. Costs 2 credits.
      tags: [Link Graph]
      parameters:
        - $ref: '#/components/parameters/domain'
        - $ref: '#/components/parameters/limit'
      responses:
        '200':
          description: Anchor text distribution
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/EnvelopeFields'
                  - type: object
                    required: [data]
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/AnchorTextRow'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/top-pages:
    get:
      operationId: getTopPages
      summary: Get top pages by inbound links
      description: >
        Returns pages on the domain ranked by number of inbound links. If the complete URL
        aggregate cannot finish, the response may contain a degraded raw fallback marked with
        `partial: true`. Costs 2 credits.
      tags: [Link Graph]
      parameters:
        - $ref: '#/components/parameters/domain'
        - $ref: '#/components/parameters/limit'
      responses:
        '200':
          description: Top pages
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/EnvelopeFields'
                  - type: object
                    required: [data]
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/TopPageRow'
                      partial:
                        type: boolean
                        description: When true, data contains a degraded raw fallback and is not a complete URL aggregate.
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/domain-authority:
    get:
      operationId: getDomainAuthority
      summary: Get domain authority score
      description: >
        Returns a 0-100 authority score based on referring domains, enriched with RDAP registration data
        and Tranco popularity rank. Costs 1 credit.
      tags: [Domain Metrics]
      parameters:
        - $ref: '#/components/parameters/domain'
      responses:
        '200':
          description: Domain authority data
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/EnvelopeFields'
                  - type: object
                    required: [data]
                    properties:
                      data:
                        $ref: '#/components/schemas/DomainAuthority'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/domain-rank:
    get:
      operationId: getDomainRank
      summary: Get domain rank metrics
      description: Returns inbound edge count, unique linking domains, and average linking host count. Costs 2 credits.
      tags: [Domain Metrics]
      parameters:
        - $ref: '#/components/parameters/domain'
      responses:
        '200':
          description: Domain rank data
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/EnvelopeFields'
                  - type: object
                    required: [data]
                    properties:
                      data:
                        $ref: '#/components/schemas/DomainRank'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/crawl-history:
    get:
      operationId: getCrawlHistory
      summary: Get crawl history for a domain
      description: Returns first seen date, last seen date, and total URL count from the Common Crawl index. Costs 2 credits.
      tags: [Domain Metrics]
      parameters:
        - $ref: '#/components/parameters/domain'
      responses:
        '200':
          description: Crawl history
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/EnvelopeFields'
                  - type: object
                    required: [data]
                    properties:
                      data:
                        $ref: '#/components/schemas/CrawlHistory'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/tech-stack:
    get:
      operationId: getTechStack
      summary: Get detected tech stack
      description: >
        Fetches the domain in real time and returns detected server software, CMS, and framework
        signals from HTTP headers and meta tags. Costs 2 credits.
      tags: [Page Analysis]
      parameters:
        - $ref: '#/components/parameters/domain'
      responses:
        '200':
          description: Tech stack data
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/EnvelopeFields'
                  - type: object
                    required: [data]
                    properties:
                      data:
                        $ref: '#/components/schemas/TechStack'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/schema-markup:
    get:
      operationId: getSchemaMarkup
      summary: Extract JSON-LD structured data from a URL
      description: >
        Fetches the target URL in real time and extracts every schema.org JSON-LD block
        (<script type="application/ld+json">), including entities nested inside @graph arrays.
        Malformed blocks are skipped and reported individually rather than failing the whole
        request. Costs 2 credits.
      tags: [Page Analysis]
      parameters:
        - name: url
          in: query
          required: true
          schema:
            type: string
          description: The full URL to analyze (e.g. "https://example.com/product")
      responses:
        '200':
          description: Extracted schema.org entities
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/EnvelopeFields'
                  - type: object
                    required: [data, final_url, status_code, response_time_ms, has_schema_markup, types, block_count, valid_block_count, invalid_block_count, invalid_blocks]
                    properties:
                      data:
                        type: array
                        description: One entry per schema.org entity found (flattened across blocks and @graph).
                        items:
                          $ref: '#/components/schemas/SchemaMarkupEntity'
                      final_url:
                        type: string
                        description: The URL after following redirects.
                      status_code:
                        type: integer
                      response_time_ms:
                        type: integer
                      has_schema_markup:
                        type: boolean
                        description: True when at least one entity was successfully extracted.
                      types:
                        type: array
                        description: Unique schema.org types found across every entity on the page, sorted alphabetically.
                        items:
                          type: string
                      block_count:
                        type: integer
                        description: Total <script type="application/ld+json"> blocks found on the page.
                      valid_block_count:
                        type: integer
                      invalid_block_count:
                        type: integer
                      invalid_blocks:
                        type: array
                        description: Malformed blocks are skipped (not fatal) but reported here for diagnosis.
                        items:
                          $ref: '#/components/schemas/SchemaMarkupInvalidBlock'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          description: Target URL unreachable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /v1/page-seo:
    get:
      operationId: getPageSeo
      summary: Full SEO audit for a URL
      description: >
        Fetches the target URL in real time and returns a complete SEO audit including meta tags,
        Open Graph, JSON-LD structured data, hreflang, headings, image alt coverage, internal/external
        links, word count, and more. Returns 503 if the URL is unreachable. Costs 3 credits.
      tags: [Page Analysis]
      parameters:
        - name: url
          in: query
          required: true
          schema:
            type: string
          description: The full URL to analyze (e.g. "https://example.com/about")
      responses:
        '200':
          description: Page SEO audit
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/EnvelopeFields'
                  - type: object
                    required: [data]
                    properties:
                      data:
                        $ref: '#/components/schemas/PageSeoData'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          description: Target URL unreachable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /v1/site-health:
    get:
      operationId: getSiteHealth
      summary: Real-time site health check
      description: >
        Probes key URLs in real time to check HTTPS enforcement, HSTS, www redirect, security headers,
        robots.txt presence, and sitemap availability. Costs 2 credits.
      tags: [Page Analysis]
      parameters:
        - $ref: '#/components/parameters/domain'
      responses:
        '200':
          description: Site health report
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/EnvelopeFields'
                  - type: object
                    required: [data]
                    properties:
                      data:
                        $ref: '#/components/schemas/SiteHealthData'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/sitemap:
    get:
      operationId: getSitemap
      summary: Discover and parse a domain's sitemap
      description: >
        Discovers a domain's sitemap via every Sitemap directive in robots.txt, falling back to
        /sitemap.xml and /sitemap_index.xml, then parses and returns the URL inventory with
        lastmod, changefreq, and priority. Sitemap indexes are walked breadth-first including
        indexes nested one level below the root, bounded to 16 documents and a 12-second budget;
        `truncated` is true when a bound stopped the walk early. Returns empty data (not an
        error) when no sitemap is found, and refunds the charge in that case (credits_used 0).
        The legacy `/v1/url-index` route redirects here (301). Costs 2 credits.
      tags: [Page Analysis]
      parameters:
        - $ref: '#/components/parameters/domain'
        - $ref: '#/components/parameters/limit'
      responses:
        '200':
          description: Sitemap URL inventory
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/EnvelopeFields'
                  - type: object
                    required: [data, total, sitemap_url, sitemap_type, child_sitemaps, child_sitemaps_total, child_sitemaps_fetched, truncated, source]
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/SitemapEntry'
                      total:
                        type: integer
                        minimum: 0
                      sitemap_url:
                        type: string
                        nullable: true
                      sitemap_type:
                        type: string
                        description: Type of the root sitemap document.
                        enum: [sitemap, sitemap_index, none]
                      child_sitemaps:
                        type: array
                        description: >
                          Child sitemaps discovered in the index, capped at 200 entries.
                          See child_sitemaps_total for the true count.
                        items:
                          type: string
                      child_sitemaps_total:
                        type: integer
                        minimum: 0
                        description: Number of child sitemaps discovered, before child_sitemaps is capped.
                      child_sitemaps_fetched:
                        type: array
                        description: The child documents actually read while building this response.
                        items:
                          type: string
                      truncated:
                        type: boolean
                        description: >
                          True when a document or time bound stopped the walk early,
                          so data and total under-report the site.
                      source:
                        type: string
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/page-performance:
    get:
      operationId: getPagePerformance
      summary: Page performance and Core Web Vitals
      description: >
        Runs a Google PageSpeed Insights analysis and returns Lighthouse scores plus Core Web Vitals
        (LCP, CLS, TBT, FCP, Speed Index, TTI). Results are cached for 24 hours.
        Subject to a global cap of 25,000 calls/day and 50 calls/day per user.
        Costs 3 credits (deducted only on successful PSI response).
      tags: [Page Analysis]
      parameters:
        - name: url
          in: query
          required: true
          schema:
            type: string
          description: The full URL to analyze
        - name: strategy
          in: query
          required: false
          schema:
            type: string
            enum: [mobile, desktop]
            default: mobile
          description: Device strategy (default mobile)
      responses:
        '200':
          description: Performance scores and Core Web Vitals
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/EnvelopeFields'
                  - type: object
                    required: [data]
                    properties:
                      data:
                        $ref: '#/components/schemas/PagePerformanceData'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '429':
          description: Daily quota reached (global or per-user)
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Error'
                  - type: object
                    properties:
                      retry_after:
                        type: string
        '503':
          description: PageSpeed Insights API unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /v1/domain-overlap:
    get:
      operationId: getDomainOverlap
      summary: Find domains linking to multiple targets
      description: >
        Returns domains that link to two or more of the specified target domains.
        Requires at least 2 domains and allows up to 5. Costs 5 credits.
      tags: [Cross-Domain]
      parameters:
        - $ref: '#/components/parameters/domainsCsv'
        - $ref: '#/components/parameters/limit'
      responses:
        '200':
          description: Overlapping domains
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/EnvelopeFields'
                  - type: object
                    required: [data]
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/DomainOverlapRow'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/link-intersect:
    get:
      operationId: getLinkIntersect
      summary: Find domains linking to both targets
      description: Returns domains that link to both domain_a and domain_b. Useful for finding shared link sources. Costs 5 credits.
      tags: [Cross-Domain]
      parameters:
        - name: domain_a
          in: query
          required: true
          schema:
            type: string
          description: First domain to compare
        - name: domain_b
          in: query
          required: true
          schema:
            type: string
          description: Second domain to compare
        - $ref: '#/components/parameters/limit'
      responses:
        '200':
          description: Intersecting linker domains
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/EnvelopeFields'
                  - type: object
                    required: [data]
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/LinkIntersectRow'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/competitor-gap:
    get:
      operationId: getCompetitorGap
      summary: Find competitor link gaps
      description: >
        Returns domains that link to the competitor (`vs`) but not to the target domain.
        Use this to discover link-building opportunities where competitors have coverage you don't.
        Generic hosting platforms (blogspot.com, wixsite.com, myshopify.com, github.io,
        vercel.app and similar) are excluded by default, because every site hosted on one rolls
        up to the platform apex and crowds out real prospects; pass include_generic=true for the
        unfiltered set. Costs 5 credits.
      tags: [Cross-Domain]
      parameters:
        - $ref: '#/components/parameters/domain'
        - name: vs
          in: query
          required: true
          schema:
            type: string
          description: Competitor domain to compare against
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            default: 50
            minimum: 1
            maximum: 200
          description: Maximum results to return (default 50, max 200)
        - name: include_generic
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: >
            Keep generic hosting platforms in the results. Off by default.
      responses:
        '200':
          description: Competitor gap domains
          content:
            application/json:
              schema:
                type: object
                required: [domain, vs, data, total, returned, partial, scored, budget_ms, credits_used, credits_remaining]
                properties:
                  domain:
                    type: string
                  vs:
                    type: string
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/CompetitorGapRow'
                  total:
                    type: integer
                    minimum: 0
                  returned:
                    type: integer
                    minimum: 0
                  partial:
                    type: boolean
                    description: >
                      True when the query budget was reached before the full referring-domain
                      set was compared, so data under-reports the gap.
                  scored:
                    type: boolean
                    description: True when rows carry a referring-domain authority score.
                  budget_ms:
                    type: integer
                    minimum: 0
                    description: The wall-clock budget this request was allowed, in milliseconds.
                  credits_used:
                    type: integer
                    minimum: 0
                  credits_remaining:
                    type: integer
                    minimum: 0
                  crawl_release:
                    type: string
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/similar-domains:
    get:
      operationId: getSimilarDomains
      summary: Find similar domains
      description: >
        Returns domains that share linking domains with the target — a signal of topical proximity.
        Limit is capped at 100. Costs 5 credits.
      tags: [Cross-Domain]
      parameters:
        - $ref: '#/components/parameters/domain'
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            default: 100
            minimum: 1
            maximum: 100
          description: Maximum results (default 100, max 100)
      responses:
        '200':
          description: Similar domains
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/EnvelopeFields'
                  - type: object
                    required: [data]
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/SimilarDomainRow'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/link-audit:
    get:
      operationId: getLinkAudit
      summary: Run link profile audit
      description: >
        Returns a composite backlink quality score, anchor text profile, risk flags, and outbound
        link concentration signals in a single request. Costs 8 credits.
      tags: [Power Endpoints]
      parameters:
        - $ref: '#/components/parameters/domain'
      responses:
        '200':
          description: Link audit report
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LinkAudit'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/site-explorer:
    get:
      operationId: getSiteExplorer
      summary: Full site overview
      description: >
        Returns backlinks, authority score, top pages, and anchor text distribution in a single request.
        Costs 10 credits.
      tags: [Power Endpoints]
      parameters:
        - $ref: '#/components/parameters/domain'
      responses:
        '200':
          description: Site explorer data
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/EnvelopeFields'
                  - type: object
                    required: [data]
                    properties:
                      data:
                        $ref: '#/components/schemas/SiteExplorer'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/batch:
    post:
      operationId: postBatch
      summary: Batch backlink lookup
      description: >
        Looks up backlinks for up to 50 domains in a single request.
        Costs 2 credits per successfully queried domain (charged after queries complete).
      tags: [Power Endpoints]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domains]
              properties:
                domains:
                  type: array
                  items:
                    type: string
                  minItems: 1
                  maxItems: 50
                  description: Domains to look up backlinks for
      responses:
        '200':
          description: Batch results
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/EnvelopeFields'
                  - type: object
                    required: [data]
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/BatchResultItem'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/rank-tracking/sites:
    get:
      operationId: listRankTrackingSites
      summary: List rank-tracking websites
      description: >
        Lists the logical RankParse websites configured for stored
        Google/Bing/Yandex keyword tracking, including each engine's attached
        property and sync freshness. Reads snapshots synced on a schedule —
        never a live SERP check. Read-only, API-key auth, 0 credits.
      tags: [Rank Tracking]
      responses:
        '200':
          description: Rank-tracking websites for this API key
          content:
            application/json:
              schema:
                type: object
                required: [sites]
                properties:
                  sites:
                    type: array
                    items:
                      $ref: '#/components/schemas/RankTrackingSite'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/rank-tracking/keywords:
    get:
      operationId: listTrackedKeywords
      summary: List tracked keywords for a website
      description: >
        Lists the tracked keyword IDs and query text for one rank-tracking
        website. Read-only, API-key auth, 0 credits.
      tags: [Rank Tracking]
      parameters:
        - $ref: '#/components/parameters/rankTrackingSiteId'
        - $ref: '#/components/parameters/rankTrackingLimit'
        - $ref: '#/components/parameters/offset'
      responses:
        '200':
          description: Tracked keywords for the site
          content:
            application/json:
              schema:
                type: object
                required: [site_id, canonical_host, keywords, total, limit, offset]
                properties:
                  site_id:
                    type: string
                  canonical_host:
                    type: string
                  keywords:
                    type: array
                    items:
                      $ref: '#/components/schemas/TrackedKeyword'
                  total:
                    type: integer
                    minimum: 0
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: No rank-tracking site with that `site_id` belongs to this API key (code `rank_tracking_site_not_found`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/rank-tracking/rankings:
    get:
      operationId: getKeywordRankings
      summary: Get keyword ranking summaries
      description: >
        Returns stored current-vs-previous-period ranking summaries for a
        website's tracked keywords across Google, Bing, and Yandex. Reads
        snapshots synced on a schedule — never a live neutral-location SERP
        check, and returned positions are provider-reported averages over
        each engine's own period, not comparable as one blended rank across
        engines. `engine=all` (the default) returns every engine enabled for
        this account side by side; each keyword's `engines` object only has
        keys for engines actually attached to this site, so compare it
        against the response's `enabled_engines` to tell "disabled for my
        account" apart from "enabled but not attached here". A null
        current/previous `position` on a present engine, together with an
        `availability` other than `available` (e.g. `provider_limited`,
        `pending_first_sync`, `stale`, `sync_failed`), signals a data-quality
        or sync-timing gap for that engine — not evidence the keyword ranks
        nowhere. Read-only, API-key auth, 0 credits.
      tags: [Rank Tracking]
      parameters:
        - $ref: '#/components/parameters/rankTrackingSiteId'
        - $ref: '#/components/parameters/rankTrackingKeywordIdOptional'
        - $ref: '#/components/parameters/rankTrackingEngineAll'
        - $ref: '#/components/parameters/rankTrackingPeriodDays'
        - $ref: '#/components/parameters/rankTrackingLimit'
        - $ref: '#/components/parameters/offset'
      responses:
        '200':
          description: Keyword ranking summaries
          content:
            application/json:
              schema:
                type: object
                required: [site_id, canonical_host, enabled_engines, period_days, limit, offset, total, keywords]
                properties:
                  site_id:
                    type: string
                  canonical_host:
                    type: string
                  enabled_engines:
                    type: array
                    description: >
                      Engines enabled for this account, independent of which
                      engines actually had data for these keywords.
                    items:
                      $ref: '#/components/schemas/RankTrackingEngine'
                  period_days:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
                  total:
                    type: integer
                    minimum: 0
                  keywords:
                    type: array
                    items:
                      $ref: '#/components/schemas/RankTrackingKeywordSummary'
              examples:
                allEngines:
                  summary: engine=all — Google available, Bing provider_limited (null metrics, not "unranked")
                  value:
                    site_id: site_abc123
                    canonical_host: example.com
                    enabled_engines: [google, bing, yandex]
                    period_days: 28
                    limit: 100
                    offset: 0
                    total: 1
                    keywords:
                      - id: kw_1
                        query: buy widgets
                        engines:
                          google:
                            engine: google
                            property_id: prop_g1
                            property_url: https://example.com/
                            availability: available
                            data_as_of: '2026-08-25'
                            top_page: https://example.com/widgets
                            current_period: { start_date: '2026-07-29', end_date: '2026-08-25' }
                            previous_period: { start_date: '2026-07-01', end_date: '2026-07-28' }
                            current: { clicks: 12, impressions: 340, ctr: 0.035, position: 6.2, click_position: 5.9 }
                            previous: { clicks: 9, impressions: 310, ctr: 0.029, position: 7.1, click_position: 6.8 }
                            movement: 0.9
                          bing:
                            engine: bing
                            property_id: prop_b1
                            property_url: https://example.com/
                            availability: provider_limited
                            data_as_of: '2026-08-20'
                            top_page: null
                            current_period: { start_date: '2026-07-29', end_date: '2026-08-25' }
                            previous_period: { start_date: '2026-07-01', end_date: '2026-07-28' }
                            current: { clicks: null, impressions: null, ctr: null, position: null, click_position: null }
                            previous: { clicks: null, impressions: null, ctr: null, position: null, click_position: null }
                            movement: null
        '400':
          description: >
            Invalid `engine` (code `invalid_rank_tracking_engine`); invalid
            `period_days` (code `invalid_rank_tracking_period`); or `engine`
            is a valid engine name that is not enabled for this account (code
            `rank_tracking_provider_disabled`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: >
            No rank-tracking site with that `site_id` belongs to this API key
            (code `rank_tracking_site_not_found`); or `keyword_id` was given
            and does not belong to this API key on this site (code
            `rank_tracking_keyword_not_found`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/rank-tracking/history:
    get:
      operationId: getKeywordRankHistory
      summary: Get day-by-day keyword rank history
      description: >
        Returns the stored day-by-day ranking history for one keyword on one
        search engine over a date range. Reads snapshots synced on a
        schedule — never a live SERP check. History is always single-engine,
        so `engine` is required and `all` is not accepted here (call
        rankings first to compare engines). Days with no synced observation
        are simply absent from `rows` rather than filled with a zero or
        placeholder row — never treat a missing date as "not ranking". Bing
        and Yandex are opt-in and lower-frequency than Google; their rows may
        carry `availability: provider_limited`, meaning that provider did not
        return enough data for that day, not that the keyword ranked
        nowhere. Read-only, API-key auth, 0 credits.
      tags: [Rank Tracking]
      parameters:
        - $ref: '#/components/parameters/rankTrackingSiteId'
        - name: keyword_id
          in: query
          required: true
          schema:
            type: string
          description: Tracked keyword ID, from `GET /v1/rank-tracking/keywords`.
        - $ref: '#/components/parameters/rankTrackingEngineRequired'
        - $ref: '#/components/parameters/rankTrackingStartDate'
        - $ref: '#/components/parameters/rankTrackingEndDate'
      responses:
        '200':
          description: Day-by-day rank history for the keyword, ascending by date
          content:
            application/json:
              schema:
                type: object
                required: [site_id, keyword_id, query, engine, property_id, property_url, availability, data_as_of, last_attempt_at, last_success_at, error_code, start_date, end_date, rows]
                properties:
                  site_id:
                    type: string
                  keyword_id:
                    type: string
                  query:
                    type: string
                  engine:
                    $ref: '#/components/schemas/RankTrackingEngine'
                  property_id:
                    type: string
                  property_url:
                    type: string
                  availability:
                    $ref: '#/components/schemas/RankTrackingAvailability'
                  data_as_of:
                    type: string
                    nullable: true
                  last_attempt_at:
                    type: integer
                    nullable: true
                    description: Unix epoch milliseconds of the most recent sync attempt for this engine/property, or null.
                  last_success_at:
                    type: integer
                    nullable: true
                    description: Unix epoch milliseconds of the most recent successful sync, or null.
                  error_code:
                    type: string
                    nullable: true
                  start_date:
                    type: string
                  end_date:
                    type: string
                  rows:
                    type: array
                    description: >
                      Ascending by date. A missing calendar date within
                      [start_date, end_date] means no synced observation that
                      day — not a zero or bottom rank.
                    items:
                      $ref: '#/components/schemas/RankTrackingHistoryRow'
        '400':
          description: >
            Invalid or missing `engine` — `all` is not accepted here (code
            `invalid_rank_tracking_engine`); `engine` is a valid engine name
            that is not enabled for this account (code
            `rank_tracking_provider_disabled`); missing/malformed
            `start_date`/`end_date`, `start_date` after `end_date`, or an
            inclusive span over 400 days (code `invalid_rank_tracking_date`);
            or the keyword exists but has no primary property attached for
            this engine (code `rank_tracking_property_not_attached`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: No tracked keyword with that `keyword_id` belongs to this API key on this site (code `rank_tracking_keyword_not_found`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/link-velocity:
    get:
      operationId: getLinkVelocity
      summary: Link velocity (stub)
      description: >
        Not yet available in v1. Requires two crawl releases to compute.
        Returns immediately with status "not_yet_available". Costs 0 credits.
      tags: [Stubs]
      parameters:
        - name: domain
          in: query
          required: false
          schema:
            type: string
      responses:
        '200':
          description: Stub response
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/EnvelopeFields'
                  - type: object
                    required: [data]
                    properties:
                      data:
                        $ref: '#/components/schemas/StubData'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/new-links:
    get:
      operationId: getNewLinks
      summary: New links (stub)
      description: >
        Not yet available in v1. Requires two crawl releases to compute.
        Returns immediately with status "not_yet_available". Costs 0 credits.
      tags: [Stubs]
      parameters:
        - name: domain
          in: query
          required: false
          schema:
            type: string
      responses:
        '200':
          description: Stub response
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/EnvelopeFields'
                  - type: object
                    required: [data]
                    properties:
                      data:
                        $ref: '#/components/schemas/StubData'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/internal-links:
    get:
      operationId: getInternalLinks
      summary: Internal links (stub)
      description: >
        Not yet available in v1. Returns immediately with status "not_yet_available". Costs 0 credits.
      tags: [Stubs]
      parameters:
        - $ref: '#/components/parameters/domain'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
      responses:
        '200':
          description: Stub response
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/EnvelopeFields'
                  - type: object
                    required: [data]
                    properties:
                      data:
                        $ref: '#/components/schemas/StubData'
                      status:
                        type: string
                        enum: ['not_yet_available']
                      reason:
                        type: string
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/lost-links:
    get:
      operationId: getLostLinks
      summary: Lost links (stub)
      description: >
        Not yet available in v1. Requires two crawl releases to compute.
        Returns immediately with status "not_yet_available". Costs 0 credits.
      tags: [Stubs]
      parameters:
        - name: domain
          in: query
          required: false
          schema:
            type: string
      responses:
        '200':
          description: Stub response
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/EnvelopeFields'
                  - type: object
                    required: [data]
                    properties:
                      data:
                        $ref: '#/components/schemas/StubData'
        '401':
          $ref: '#/components/responses/Unauthorized'
