GoatCounter · OpenAPI Overlay 1.0.0

API Evangelist enhancements for the GoatCounter API

8 actions 8 updates update extends openapi/_original/goatcounter-api-swagger20.json
Generated by API Evangelist Written by API Evangelist tooling for GoatCounter's API. It is a proposal applied on top of the contract, not a document GoatCounter publishes.
View Overlay File View on GitHub Overlay Specification

What the actions change

hostbasePathschemesx-host-templatebearerAuthx-apievangelist-profilex-apievangelist-artifactsx-rate-limit

Targets 5

$
$.securityDefinitions
$.info
$.paths['/api/v0/stats/hits'].get.parameters[?(@.name=='daily')]
$.paths['/api/v0/export/{id}/download'].get.responses['202']

OpenAPI Overlay

Raw ↑
overlay: 1.0.0
info:
  title: API Evangelist enhancements for the GoatCounter API
  version: 1.0.0
extends: openapi/_original/goatcounter-api-swagger20.json
x-provenance:
  generated: '2026-08-13'
  method: generated
  source: >-
    Enhancements derived from https://www.goatcounter.com/help/api and from live probes on
    2026-08-13. The target document is GoatCounter's own Swagger 2.0 at
    https://www.goatcounter.com/api.json, saved verbatim in openapi/_original/. It is never
    mutated — everything API Evangelist adds is expressed here as overlay actions.
  note: >-
    The single most consequential gap in the published document is that it declares no host and no
    basePath, so a client cannot learn from the contract where to send a request. The first action
    below supplies the real per-site host pattern that the documentation states in prose. The second
    supplies the bearer scheme, which the documentation leads with but which is absent from
    securityDefinitions (only basicAuth is declared there).
actions:
  - target: $
    description: >-
      Supply the host, basePath and scheme the published document omits. GoatCounter serves the API
      from the account's own subdomain; www.goatcounter.com/api/v0/me returns 404 and
      goatcounter.com/api/v0/me returns 301, both probed 2026-08-13.
    update:
      host: '{code}.goatcounter.com'
      basePath: /api/v0
      schemes:
        - https
      x-host-template:
        variable: code
        description: >-
          The site's domain code, the same label that forms the subdomain — code "arp242" means
          arp242.goatcounter.com. Self-hosted instances substitute their own hostname entirely.
        source: https://www.goatcounter.com/help/api
  - target: $.securityDefinitions
    description: >-
      Add the bearer scheme documented on the API help page. The published document declares only
      basicAuth, so an agent reading the contract alone would miss the primary auth method.
    update:
      bearerAuth:
        type: apiKey
        name: Authorization
        in: header
        description: >-
          'Authorization: Bearer <token>'. Create a key in the GoatCounter dashboard under
          [Username in top menu] -> API. Swagger 2.0 has no native bearer type, so this is expressed
          as an apiKey in the Authorization header. Documented at https://www.goatcounter.com/help/api
  - target: $.info
    description: Attach the API Evangelist profile and the artifacts derived from this contract.
    update:
      x-apievangelist-profile: https://apis.io/provider/goatcounter/
      x-apievangelist-artifacts:
        authentication: authentication/goatcounter-authentication.yml
        conventions: conventions/goatcounter-conventions.yml
        errors: errors/goatcounter-problem-types.yml
        data_model: data-model/goatcounter-data-model.yml
        lifecycle: lifecycle/goatcounter-lifecycle.yml
        rate_limits: rate-limits/goatcounter-rate-limits.yml
        skills: skills/_index.yml
  - target: $
    description: >-
      Document the rate-limit response headers, observed live on 2026-08-13. They are described in
      prose on the help page but appear nowhere in the contract.
    update:
      x-rate-limit:
        limit: 4
        unit: requests_per_second
        headers:
          X-Rate-Limit-Limit: Number of requests at which the rate limit kicks in; always the same.
          X-Rate-Limit-Remaining: Requests remaining this period.
          X-Rate-Limit-Reset: Seconds until the rate limit resets.
        exhaustion_status: undocumented
        source: https://www.goatcounter.com/help/api
  - target: $
    description: >-
      Document the error envelope contract stated on the help page — the invariant that a 4xx/5xx
      always carries either `error` or `errors` but never both is not expressible in the schemas.
    update:
      x-error-envelope:
        shapes:
          - {field: error, type: string, example: '{"error": "oh noes!"}'}
          - {field: errors, type: object, example: '{"errors": {"key": ["error1", "error2"]}}'}
        invariants:
          - A 2xx status will never contain errors.
          - A 4xx or 5xx status will always have either error or errors, but never both.
        rfc9457: false
        source: https://www.goatcounter.com/help/api
  - target: $
    description: State the absence of an idempotency contract, so an agent does not assume retry safety.
    update:
      x-idempotency:
        supported: false
        note: >-
          No idempotency key on any write operation. Retrying POST /api/v0/count will double-count
          the batch; retries must be guarded client-side.
  - target: $.paths['/api/v0/stats/hits'].get.parameters[?(@.name=='daily')]
    description: >-
      Mark the daily parameter deprecated. The published document already says so in its description
      ("Deprecated: identical to group=day and will be removed in the future") but does not set the
      machine-readable flag, so tooling does not surface it.
    update:
      x-deprecated: true
      x-replaced-by: group=day
  - target: $.paths['/api/v0/export/{id}/download'].get.responses['202']
    description: >-
      Clarify that 202 on the download endpoint is a not-ready signal in an async poll loop, not a
      success. This is the one place in the API where a 2xx carries the error envelope.
    update:
      x-async-pending: true
      x-poll: GET /api/v0/export/{id} until finished_at is non-null