APIs.io · API Governance Rules

APIs.io API Rules

Spectral linting rules defining API design standards and conventions for APIs.io.

71 Rules error 48 warn 23
View Rules File View on GitHub

Rule Categories

contact document get header info license media operation parameter path post property request response schema security server tag

Rules

error
document-openapi-32
APIs.io contracts are published as OpenAPI 3.2.0.
$.openapi
error
document-structure
A contract carries info, servers, tags, paths, and components.
$
error
info-title-required
Info must have a title.
$.info
warn
info-title-format
Titles start with "APIs.io" so every contract names its network.
$.info.title
error
info-version-required
Info must have a version.
$.info
warn
info-version-semver
Versions are numeric semver (1.6.0), not marketing strings.
$.info.version
error
info-description-substantive
The info description tells a reader what the surface is — at least a real paragraph.
$.info
error
info-contact-required
Info must say who runs the API.
$.info
error
info-license-required
Info must say what license the contract is published under.
$.info
warn
contact-complete
The contact carries a name, a URL, and an email.
$.info.contact
warn
license-complete
The license carries a name and a URL.
$.info.license
error
server-url-https
Server URLs must use HTTPS.
$.servers[*].url
warn
server-url-expected-domain
APIs.io contracts serve from the apis.io domain.
$.servers[*].url
warn
server-url-no-trailing-slash
Server URLs carry no trailing slash — paths supply their own.
$.servers[*].url
warn
server-description-required
Every server says which environment it is.
$.servers[*]
error
tag-name-required
Every tag has a name.
$.tags[*]
error
tag-description-required
Every tag describes the surface it groups.
$.tags[*]
warn
tag-name-title-case
Tag names read as titles.
$.tags[*].name
warn
path-kebab-case
Path segments are kebab-case (or a {parameter}).
$.paths[*]~
error
path-no-trailing-slash
Paths carry no trailing slash.
$.paths[*]~
error
path-declares-an-operation
A path item without an operation documents nothing.
$.paths[*]
error
operation-summary-required
Every operation has a summary.
$.paths[*][get,put,post,delete,options,head,patch,trace,query]
warn
operation-summary-sentence
Summaries read as sentences — capitalised, 10–120 characters, closed with punctuation.
$.paths[*][get,put,post,delete,options,head,patch,trace,query].summary
error
operation-description-required
Every operation says what it returns and how its filters behave.
$.paths[*][get,put,post,delete,options,head,patch,trace,query]
error
operation-id-required
Every operation has an operationId.
$.paths[*][get,put,post,delete,options,head,patch,trace,query]
error
operation-id-camel-case
OperationIds are camelCase.
$.paths[*][get,put,post,delete,options,head,patch,trace,query].operationId
error
operation-tags-required
Every operation carries at least one tag.
$.paths[*][get,put,post,delete,options,head,patch,trace,query]
error
operation-security-declared
Every operation states its security requirement — an explicit empty requirement for keyless calls, never silence.
$.paths[*][get,put,post,delete,options,head,patch,trace,query]
error
operation-tier-declared
Every operation declares its commercial tier via x-tier.
$.paths[*][get,put,post,delete,options,head,patch,trace,query]
warn
operation-mcp-tool-declared
Every operation names the MCP tool that serves the same call — the API → MCP experience chain is 1:1.
$.paths[*][get,put,post,delete,options,head,patch,trace,query]
error
operation-2xx-response
Every operation documents its success shape.
$.paths[*][get,put,post,delete,options,head,patch,trace,query].responses
error
operation-get-by-id-documents-404
A GET addressing a named resource documents the 404 a miss returns.
$.paths[?(@property.match(/\{/))][get].responses
error
operation-gated-documents-402
A gated operation documents the 402 the live API answers keyless callers with — measured, 403 is never returned.
$.paths[*][get,put,post,delete,options,head,patch,trace,query]
error
get-operations-have-no-request-body
GET operations carry no request body.
$.paths[*].get
error
post-operations-have-request-body
POST operations document what they accept.
$.paths[*].post
error
parameter-name-required
Every parameter has a name.
$.paths[*][get,put,post,delete,options,head,patch,trace,query].parameters[*]$.components.parameters[*]
error
parameter-in-required
Every parameter says where it lives.
$.paths[*][get,put,post,delete,options,head,patch,trace,query].parameters[*]$.components.parameters[*]
error
parameter-description-required
Every parameter is described.
$.paths[*][get,put,post,delete,options,head,patch,trace,query].parameters[*]$.components.parameters[*]
error
parameter-schema-required
Every parameter carries a schema.
$.paths[*][get,put,post,delete,options,head,patch,trace,query].parameters[*]$.components.parameters[*]
error
parameter-name-snake-case
Parameter names are snake_case.
$.paths[*][get,put,post,delete,options,head,patch,trace,query].parameters[*].name$.components.parameters[*].name
error
parameter-path-required-true
Path parameters are always required.
$.paths[*][get,put,post,delete,options,head,patch,trace,query].parameters[?(@.in=='path')]$.components.parameters[?(@.in=='path')]
warn
parameter-schema-bounded
No parameter is unbounded on the wire — a maxLength, maximum, enum, or maxItems (booleans are closed already).
$.paths[*][get,put,post,delete,options,head,patch,trace,query].parameters[*].schema$.components.parameters[*].schema
error
request-body-description-required
Every request body says what belongs in it.
$.paths[*][get,put,post,delete,options,head,patch,trace,query].requestBody
error
request-body-content-required
Every request body defines its content.
$.paths[*][get,put,post,delete,options,head,patch,trace,query].requestBody
warn
request-body-required-declared
Every request body states whether it is required, explicitly.
$.paths[*][get,put,post,delete,options,head,patch,trace,query].requestBody
error
request-body-json
Request bodies speak application/json.
$.paths[*][get,put,post,delete,options,head,patch,trace,query].requestBody.content
error
media-type-schema-required
Every media type carries a schema — a payload with no schema is undocumented.
$.paths[*][get,put,post,delete,options,head,patch,trace,query].responses[*].content[*]$.paths[*][get,put,post,delete,options,head,patch,trace,query].requestBody.content[*]$.components.responses[*].content[*]
warn
media-type-schema-typed
A media type schema declares its type or points at a named component.
$.paths[*][get,put,post,delete,options,head,patch,trace,query].responses[*].content[*].schema$.paths[*][get,put,post,delete,options,head,patch,trace,query].requestBody.content[*].schema$.components.responses[*].content[*].schema
warn
media-type-supported
APIs.io answers in application/json, or application/problem+json for failures — nothing else.
$.paths[*][get,put,post,delete,options,head,patch,trace,query].responses[*].content$.paths[*][get,put,post,delete,options,head,patch,trace,query].requestBody.content$.components.responses[*].content
error
response-description-required
Every response is described.
$.paths[*][get,put,post,delete,options,head,patch,trace,query].responses[*]$.components.responses[*]
error
response-code-valid
Response keys are HTTP status codes (or default).
$.paths[*][get,put,post,delete,options,head,patch,trace,query].responses
error
response-content-required
Every response except 204 defines its body.
$.paths[*][get,put,post,delete,options,head,patch,trace,query].responses[?(@property != '204')]$.components.responses[*]
warn
response-error-uses-problem-json
Failures are RFC 9457 Problem Details, served as application/problem+json.
$.paths[*][get,put,post,delete,options,head,patch,trace,query].responses[?(@property.match(/^[45]/))]
warn
response-error-carries-example
Every shared failure response shows a real example body.
$.components.responses[*].content[*]
warn
response-rate-limit-headers
Every response documents the four rate-limit headers the live API actually sends — measured on a 200, a 404 and a 402.
$.paths[*][get,put,post,delete,options,head,patch,trace,query].responses[*]$.components.responses[*]
error
response-header-described
Every header a response declares is described.
$.paths[*][get,put,post,delete,options,head,patch,trace,query].responses[*].headers[*]$.components.responses[*].headers[*]
error
response-header-has-schema
Every header a response declares carries a schema.
$.paths[*][get,put,post,delete,options,head,patch,trace,query].responses[*].headers[*]$.components.responses[*].headers[*]
error
header-description-required
Every declared header is described.
$.components.headers[*]
error
header-schema-required
Every declared header carries a schema.
$.components.headers[*]
warn
header-example-recommended
Every declared header shows what it looks like on the wire.
$.components.headers[*]
error
schema-name-pascal-case
Component schema names are PascalCase.
$.components.schemas
error
schema-description-required
Every named schema says what it models.
$.components.schemas[*]
error
schema-type-required
Every named schema declares its type (or composes with allOf/oneOf/anyOf).
$.components.schemas[*]
warn
schema-object-has-shape
An object schema declares properties, or says explicitly that it is open with additionalProperties.
$.components.schemas[?(@ && @.type == 'object')]
error
property-description-required
Every schema property is described — a field a reader has to guess at is not documented.
$.components.schemas[*].properties[*]$.components.schemas[*].properties[*].properties[*]$.components.schemas[*].properties[*].items.properties[*]$.components.schemas[*].properties[*].allOf[*].properties[*]$.paths[*][get,put,post,delete,options,head,patch,trace,query].requestBody.content[*].schema.properties[*]$.paths[*][get,put,post,delete,options,head,patch,trace,query].responses[*].content[*].schema.properties[*]
error
property-type-declared
Every schema property declares its type (or composes / points at a named schema).
$.components.schemas[*].properties[*]$.components.schemas[*].properties[*].properties[*]$.components.schemas[*].properties[*].items.properties[*]$.components.schemas[*].properties[*].allOf[*].properties[*]$.paths[*][get,put,post,delete,options,head,patch,trace,query].requestBody.content[*].schema.properties[*]$.paths[*][get,put,post,delete,options,head,patch,trace,query].responses[*].content[*].schema.properties[*]
warn
property-name-snake-case
Schema properties are snake_case. `baseURL` and `humanURL` are exempt by name — they are APIs.json vocabulary, the wire format the catalog is built on, and not this API's to rename.
$.components.schemas[*].properties$.components.schemas[*].properties[*].properties$.components.schemas[*].properties[*].items.properties$.components.schemas[*].properties[*].allOf[*].properties$.paths[*][get,put,post,delete,options,head,patch,trace,query].requestBody.content[*].schema.properties
warn
property-scalar-carries-example
Every scalar property shows a real value — examples are what agents and readers reach for first.
$.components.schemas[*].properties[*]$.components.schemas[*].properties[*].properties[*]$.components.schemas[*].properties[*].items.properties[*]$.components.schemas[*].properties[*].allOf[*].properties[*]$.paths[*][get,put,post,delete,options,head,patch,trace,query].requestBody.content[*].schema.properties[*]
warn
property-string-bounded
Every string property leaves the API bounded — maxLength, an enum, or a closed format.
$.components.schemas[*].properties[*]$.components.schemas[*].properties[*].properties[*]$.components.schemas[*].properties[*].items.properties[*]$.components.schemas[*].properties[*].allOf[*].properties[*]$.paths[*][get,put,post,delete,options,head,patch,trace,query].requestBody.content[*].schema.properties[*]
error
security-scheme-description-required
Every security scheme explains how keys behave — including that gated calls answer 402, not a refusal.
$.components.securitySchemes[*]
error
security-scheme-shape
The APIs.io scheme is an apiKey in the X-API-Key header.
$.components.securitySchemes[*]

Spectral Ruleset

Raw ↑
# authorship: generated by API Evangelist tooling. Stamped 2026-08-18 per
# roadmap#64; rebuilt 2026-09-03 for full-surface coverage.
# x-method: generated
#
# apis-io — Spectral ruleset, full-surface edition.
#
# One rule family per OpenAPI object, so that every node of every published
# APIs.io contract — info, contact, license, servers, tags, paths, operations,
# parameters, request bodies, media types, responses, headers, schemas,
# properties, security schemes — is inspected by at least one rule. Coverage is
# measured by the API Evangelist Governance API (scripts/govern.mjs), and the
# contracts are brought up to these rules by scripts/harden-openapi.mjs.
#
# Provenance — measured, not assumed (2026-09-03, 17 contracts, 95 operations):
#   - openapi 3.2.0 across all 17 (the network-wide directive of 2026-08-09).
#   - oas3-schema is OFF, deliberately: Spectral 6.x has no 3.2 document schema
#     and silently falls back to the 3.0 one, flagging valid 3.1/3.2 keywords
#     (`examples` on a schema) as errors — 111 phantom errors vs 32 real ones
#     when the same documents are checked under the 3.1 schema, which 3.2
#     extends. The structure-* rules below assert the required-field constraints
#     the schema check would have carried, correctly, for 3.2.
#   - gated operations answer 402, never 403 (probed live: /ratings, /compare,
#     /insights/company/{slug}/gaps, /areas/{slug}/leaders).
#   - every response carries the four rate-limit headers (probed live on a 200,
#     a 404 and a 402).
#   - casing: operationIds camelCase, parameters snake_case, schema names
#     PascalCase, schema properties snake_case — with `baseURL` and `humanURL`
#     exempted by name: they are APIs.json vocabulary, the wire format the
#     catalog itself is built on, and are not this API's to rename.
#   - every string leaves the API bounded: maxLength, enum, or a closed format.
extends:
  - spectral:oas
rules:
  # oas3-schema validates 3.2 documents against the 3.0 JSON Schema — every
  # `examples` array it flags is legal 3.1/3.2. See provenance note above.
  oas3-schema: off

  # ---------------------------------------------------------------- document
  document-openapi-32:
    description: APIs.io contracts are published as OpenAPI 3.2.0.
    severity: error
    given: $.openapi
    then:
      function: pattern
      functionOptions:
        match: ^3\.2\.
  document-structure:
    description: A contract carries info, servers, tags, paths, and components.
    severity: error
    given: $
    then:
      function: schema
      functionOptions:
        schema:
          type: object
          required: [openapi, info, servers, tags, paths, components]

  # -------------------------------------------------------------------- info
  info-title-required:
    description: Info must have a title.
    severity: error
    given: $.info
    then:
      field: title
      function: truthy
  info-title-format:
    description: Titles start with "APIs.io" so every contract names its network.
    severity: warn
    given: $.info.title
    then:
      function: pattern
      functionOptions:
        match: ^APIs\.io
  info-version-required:
    description: Info must have a version.
    severity: error
    given: $.info
    then:
      field: version
      function: truthy
  info-version-semver:
    description: Versions are numeric semver (1.6.0), not marketing strings.
    severity: warn
    given: $.info.version
    then:
      function: pattern
      functionOptions:
        match: ^\d+\.\d+(\.\d+)?$
  info-description-substantive:
    description: The info description tells a reader what the surface is — at least a real paragraph.
    severity: error
    given: $.info
    then:
      field: description
      function: pattern
      functionOptions:
        match: '[\s\S]{80,}'
  info-contact-required:
    description: Info must say who runs the API.
    severity: error
    given: $.info
    then:
      field: contact
      function: truthy
  info-license-required:
    description: Info must say what license the contract is published under.
    severity: error
    given: $.info
    then:
      field: license
      function: truthy
  contact-complete:
    description: The contact carries a name, a URL, and an email.
    severity: warn
    given: $.info.contact
    then:
      function: schema
      functionOptions:
        schema:
          type: object
          required: [name, url]
  license-complete:
    description: The license carries a name and a URL.
    severity: warn
    given: $.info.license
    then:
      function: schema
      functionOptions:
        schema:
          type: object
          anyOf:
            - required: [name, url]
            - required: [name, identifier]

  # ----------------------------------------------------------------- servers
  server-url-https:
    description: Server URLs must use HTTPS.
    severity: error
    given: $.servers[*].url
    then:
      function: pattern
      functionOptions:
        match: ^https://
  server-url-expected-domain:
    description: APIs.io contracts serve from the apis.io domain.
    severity: warn
    given: $.servers[*].url
    then:
      function: pattern
      functionOptions:
        match: apis\.io
  server-url-no-trailing-slash:
    description: Server URLs carry no trailing slash — paths supply their own.
    severity: warn
    given: $.servers[*].url
    then:
      function: pattern
      functionOptions:
        notMatch: /$
  server-description-required:
    description: Every server says which environment it is.
    severity: warn
    given: $.servers[*]
    then:
      field: description
      function: truthy

  # -------------------------------------------------------------------- tags
  tag-name-required:
    description: Every tag has a name.
    severity: error
    given: $.tags[*]
    then:
      field: name
      function: truthy
  tag-description-required:
    description: Every tag describes the surface it groups.
    severity: error
    given: $.tags[*]
    then:
      field: description
      function: truthy
  tag-name-title-case:
    description: Tag names read as titles.
    severity: warn
    given: $.tags[*].name
    then:
      function: pattern
      functionOptions:
        match: ^[A-Z]

  # ------------------------------------------------------------------- paths
  path-kebab-case:
    description: Path segments are kebab-case (or a {parameter}).
    severity: warn
    given: $.paths[*]~
    then:
      function: pattern
      functionOptions:
        match: ^(/([a-z0-9\-{}._]+))*$
  path-no-trailing-slash:
    description: Paths carry no trailing slash.
    severity: error
    given: $.paths[*]~
    then:
      function: pattern
      functionOptions:
        notMatch: /$
  path-declares-an-operation:
    description: A path item without an operation documents nothing.
    severity: error
    given: $.paths[*]
    then:
      function: schema
      functionOptions:
        schema:
          type: object
          anyOf:
            - required: [get]
            - required: [post]
            - required: [put]
            - required: [patch]
            - required: [delete]
            - required: [head]
            - required: [options]
            - required: [query]

  # -------------------------------------------------------------- operations
  operation-summary-required:
    description: Every operation has a summary.
    severity: error
    given: $.paths[*][get,put,post,delete,options,head,patch,trace,query]
    then:
      field: summary
      function: truthy
  operation-summary-sentence:
    description: Summaries read as sentences — capitalised, 10–120 characters, closed with punctuation.
    severity: warn
    given: $.paths[*][get,put,post,delete,options,head,patch,trace,query].summary
    then:
      function: pattern
      functionOptions:
        match: ^[A-Z].{8,118}[.!?]$
  operation-description-required:
    description: Every operation says what it returns and how its filters behave.
    severity: error
    given: $.paths[*][get,put,post,delete,options,head,patch,trace,query]
    then:
      field: description
      function: truthy
  operation-id-required:
    description: Every operation has an operationId.
    severity: error
    given: $.paths[*][get,put,post,delete,options,head,patch,trace,query]
    then:
      field: operationId
      function: truthy
  operation-id-camel-case:
    description: OperationIds are camelCase.
    severity: error
    given: $.paths[*][get,put,post,delete,options,head,patch,trace,query].operationId
    then:
      function: pattern
      functionOptions:
        match: ^[a-z][a-zA-Z0-9]+$
  operation-tags-required:
    description: Every operation carries at least one tag.
    severity: error
    given: $.paths[*][get,put,post,delete,options,head,patch,trace,query]
    then:
      field: tags
      function: truthy
  operation-security-declared:
    description: Every operation states its security requirement — an explicit empty requirement for keyless calls, never silence.
    severity: error
    given: $.paths[*][get,put,post,delete,options,head,patch,trace,query]
    then:
      function: schema
      functionOptions:
        schema:
          type: object
          required: [security]
  operation-tier-declared:
    description: Every operation declares its commercial tier via x-tier.
    severity: error
    given: $.paths[*][get,put,post,delete,options,head,patch,trace,query]
    then:
      function: schema
      functionOptions:
        schema:
          type: object
          required: [x-tier]
          properties:
            x-tier:
              enum: [free, pro, business, owner]
  operation-mcp-tool-declared:
    description: Every operation names the MCP tool that serves the same call — the API → MCP experience chain is 1:1.
    severity: warn
    given: $.paths[*][get,put,post,delete,options,head,patch,trace,query]
    then:
      field: x-mcp-tool
      function: pattern
      functionOptions:
        match: ^[a-z][a-z0-9_]*$
  operation-2xx-response:
    description: Every operation documents its success shape.
    severity: error
    given: $.paths[*][get,put,post,delete,options,head,patch,trace,query].responses
    then:
      function: schema
      functionOptions:
        schema:
          type: object
          anyOf:
            - required: ['200']
            - required: ['201']
            - required: ['202']
            - required: ['204']
  # There is deliberately no blanket "must document an error" rule: the free
  # list endpoints ignore malformed filter values rather than rejecting them —
  # probed live (/tags?limit=abc, page=0, limit=1000 all answer 200) — so a
  # documented 400 there would be a fabricated failure mode. The failures an
  # operation CAN return are each forced by their own rule: 402 for gated
  # operations, 404 for a GET that addresses a named resource.
  operation-get-by-id-documents-404:
    description: A GET addressing a named resource documents the 404 a miss returns.
    severity: error
    given: $.paths[?(@property.match(/\{/))][get].responses
    then:
      function: schema
      functionOptions:
        schema:
          type: object
          required: ['404']
  operation-gated-documents-402:
    description: A gated operation documents the 402 the live API answers keyless callers with — measured, 403 is never returned.
    severity: error
    given: $.paths[*][get,put,post,delete,options,head,patch,trace,query]
    then:
      function: schema
      functionOptions:
        schema:
          type: object
          if:
            required: [x-tier]
            properties:
              x-tier:
                enum: [pro, business, owner]
          then:
            properties:
              responses:
                required: ['402']
  get-operations-have-no-request-body:
    description: GET operations carry no request body.
    severity: error
    given: $.paths[*].get
    then:
      field: requestBody
      function: falsy
  post-operations-have-request-body:
    description: POST operations document what they accept.
    severity: error
    given: $.paths[*].post
    then:
      field: requestBody
      function: truthy

  # -------------------------------------------------------------- parameters
  parameter-name-required:
    description: Every parameter has a name.
    severity: error
    given:
      - $.paths[*][get,put,post,delete,options,head,patch,trace,query].parameters[*]
      - $.components.parameters[*]
    then:
      field: name
      function: truthy
  parameter-in-required:
    description: Every parameter says where it lives.
    severity: error
    given:
      - $.paths[*][get,put,post,delete,options,head,patch,trace,query].parameters[*]
      - $.components.parameters[*]
    then:
      field: in
      function: truthy
  parameter-description-required:
    description: Every parameter is described.
    severity: error
    given:
      - $.paths[*][get,put,post,delete,options,head,patch,trace,query].parameters[*]
      - $.components.parameters[*]
    then:
      field: description
      function: truthy
  parameter-schema-required:
    description: Every parameter carries a schema.
    severity: error
    given:
      - $.paths[*][get,put,post,delete,options,head,patch,trace,query].parameters[*]
      - $.components.parameters[*]
    then:
      field: schema
      function: truthy
  parameter-name-snake-case:
    description: Parameter names are snake_case.
    severity: error
    given:
      - $.paths[*][get,put,post,delete,options,head,patch,trace,query].parameters[*].name
      - $.components.parameters[*].name
    then:
      function: pattern
      functionOptions:
        match: ^[a-z][a-z0-9_]*$
  parameter-path-required-true:
    description: Path parameters are always required.
    severity: error
    given:
      - $.paths[*][get,put,post,delete,options,head,patch,trace,query].parameters[?(@.in=='path')]
      - $.components.parameters[?(@.in=='path')]
    then:
      field: required
      function: truthy
  parameter-schema-bounded:
    description: No parameter is unbounded on the wire — a maxLength, maximum, enum, or maxItems (booleans are closed already).
    severity: warn
    given:
      - $.paths[*][get,put,post,delete,options,head,patch,trace,query].parameters[*].schema
      - $.components.parameters[*].schema
    then:
      function: schema
      functionOptions:
        schema:
          type: object
          anyOf:
            - required: [maxLength]
            - required: [maximum]
            - required: [enum]
            - required: [maxItems]
            - required: [$ref]
            - properties:
                type:
                  const: boolean
              required: [type]

  # ---------------------------------------------------------- request bodies
  request-body-description-required:
    description: Every request body says what belongs in it.
    severity: error
    given: $.paths[*][get,put,post,delete,options,head,patch,trace,query].requestBody
    then:
      field: description
      function: truthy
  request-body-content-required:
    description: Every request body defines its content.
    severity: error
    given: $.paths[*][get,put,post,delete,options,head,patch,trace,query].requestBody
    then:
      field: content
      function: truthy
  request-body-required-declared:
    description: Every request body states whether it is required, explicitly.
    severity: warn
    given: $.paths[*][get,put,post,delete,options,head,patch,trace,query].requestBody
    then:
      function: schema
      functionOptions:
        schema:
          type: object
          required: [required]
  request-body-json:
    description: Request bodies speak application/json.
    severity: error
    given: $.paths[*][get,put,post,delete,options,head,patch,trace,query].requestBody.content
    then:
      field: application/json
      function: truthy

  # ------------------------------------------------------------- media types
  media-type-schema-required:
    description: Every media type carries a schema — a payload with no schema is undocumented.
    severity: error
    given:
      - $.paths[*][get,put,post,delete,options,head,patch,trace,query].responses[*].content[*]
      - $.paths[*][get,put,post,delete,options,head,patch,trace,query].requestBody.content[*]
      - $.components.responses[*].content[*]
    then:
      field: schema
      function: truthy
  media-type-schema-typed:
    description: A media type schema declares its type or points at a named component.
    severity: warn
    given:
      - $.paths[*][get,put,post,delete,options,head,patch,trace,query].responses[*].content[*].schema
      - $.paths[*][get,put,post,delete,options,head,patch,trace,query].requestBody.content[*].schema
      - $.components.responses[*].content[*].schema
    then:
      function: schema
      functionOptions:
        schema:
          type: object
          anyOf:
            - required: [type]
            - required: [$ref]
            - required: [allOf]
            - required: [oneOf]
            - required: [anyOf]
  media-type-supported:
    description: APIs.io answers in application/json, or application/problem+json for failures — nothing else.
    severity: warn
    given:
      - $.paths[*][get,put,post,delete,options,head,patch,trace,query].responses[*].content
      - $.paths[*][get,put,post,delete,options,head,patch,trace,query].requestBody.content
      - $.components.responses[*].content
    then:
      field: '@key'
      function: pattern
      functionOptions:
        match: ^application/(json|problem\+json)$

  # --------------------------------------------------------------- responses
  response-description-required:
    description: Every response is described.
    severity: error
    given:
      - $.paths[*][get,put,post,delete,options,head,patch,trace,query].responses[*]
      - $.components.responses[*]
    then:
      field: description
      function: truthy
  response-code-valid:
    description: Response keys are HTTP status codes (or default).
    severity: error
    given: $.paths[*][get,put,post,delete,options,head,patch,trace,query].responses
    then:
      field: '@key'
      function: pattern
      functionOptions:
        match: ^([1-5][0-9]{2}|default)$
  response-content-required:
    description: Every response except 204 defines its body.
    severity: error
    given:
      - $.paths[*][get,put,post,delete,options,head,patch,trace,query].responses[?(@property != '204')]
      - $.components.responses[*]
    then:
      field: content
      function: truthy
  response-error-uses-problem-json:
    description: Failures are RFC 9457 Problem Details, served as application/problem+json.
    severity: warn
    given: $.paths[*][get,put,post,delete,options,head,patch,trace,query].responses[?(@property.match(/^[45]/))]
    then:
      function: schema
      functionOptions:
        schema:
          type: object
          properties:
            content:
              type: object
              required: [application/problem+json]
          required: [content]
  response-error-carries-example:
    description: Every shared failure response shows a real example body.
    severity: warn
    given: $.components.responses[*].content[*]
    then:
      function: schema
      functionOptions:
        schema:
          type: object
          anyOf:
            - required: [example]
            - required: [examples]
  response-rate-limit-headers:
    description: Every response documents the four rate-limit headers the live API actually sends — measured on a 200, a 404 and a 402.
    severity: warn
    given:
      - $.paths[*][get,put,post,delete,options,head,patch,trace,query].responses[*]
      - $.components.responses[*]
    then:
      function: schema
      functionOptions:
        schema:
          type: object
          properties:
            headers:
              type: object
              required: [ratelimit-policy, x-ratelimit-tier, x-ratelimit-limit, x-ratelimit-window]
          required: [headers]

  # ----------------------------------------------------------------- headers
  response-header-described:
    description: Every header a response declares is described.
    severity: error
    given:
      - $.paths[*][get,put,post,delete,options,head,patch,trace,query].responses[*].headers[*]
      - $.components.responses[*].headers[*]
    then:
      field: description
      function: truthy
  response-header-has-schema:
    description: Every header a response declares carries a schema.
    severity: error
    given:
      - $.paths[*][get,put,post,delete,options,head,patch,trace,query].responses[*].headers[*]
      - $.components.responses[*].headers[*]
    then:
      field: schema
      function: truthy
  header-description-required:
    description: Every declared header is described.
    severity: error
    given: $.components.headers[*]
    then:
      field: description
      function: truthy
  header-schema-required:
    description: Every declared header carries a schema.
    severity: error
    given: $.components.headers[*]
    then:
      field: schema
      function: truthy
  header-example-recommended:
    description: Every declared header shows what it looks like on the wire.
    severity: warn
    given: $.components.headers[*]
    then:
      function: schema
      functionOptions:
        schema:
          type: object
          anyOf:
            - required: [example]
            - required: [examples]

  # ----------------------------------------------------------------- schemas
  schema-name-pascal-case:
    description: Component schema names are PascalCase.
    severity: error
    given: $.components.schemas
    then:
      field: '@key'
      function: pattern
      functionOptions:
        match: ^[A-Z][A-Za-z0-9]*$
  schema-description-required:
    description: Every named schema says what it models.
    severity: error
    given: $.components.schemas[*]
    then:
      field: description
      function: truthy
  schema-type-required:
    description: Every named schema declares its type (or composes with allOf/oneOf/anyOf).
    severity: error
    given: $.components.schemas[*]
    then:
      function: schema
      functionOptions:
        schema:
          type: object
          anyOf:
            - required: [type]
            - required: [allOf]
            - required: [oneOf]
            - required: [anyOf]
            - required: [$ref]
  schema-object-has-shape:
    description: An object schema declares properties, or says explicitly that it is open with additionalProperties.
    severity: warn
    given: $.components.schemas[?(@ && @.type == 'object')]
    then:
      function: schema
      functionOptions:
        schema:
          type: object
          anyOf:
            - required: [properties]
            - required: [additionalProperties]

  # -------------------------------------------------------------- properties
  property-description-required:
    description: Every schema property is described — a field a reader has to guess at is not documented.
    severity: error
    given:
      - $.components.schemas[*].properties[*]
      - $.components.schemas[*].properties[*].properties[*]
      - $.components.schemas[*].properties[*].items.properties[*]
      - $.components.schemas[*].properties[*].allOf[*].properties[*]
      - $.paths[*][get,put,post,delete,options,head,patch,trace,query].requestBody.content[*].schema.properties[*]
      - $.paths[*][get,put,post,delete,options,head,patch,trace,query].responses[*].content[*].schema.properties[*]
    then:
      function: schema
      functionOptions:
        schema:
          type: object
          anyOf:
            - required: [description]
            - required: [$ref]
  property-type-declared:
    description: Every schema property declares its type (or composes / points at a named schema).
    severity: error
    given:
      - $.components.schemas[*].properties[*]
      - $.components.schemas[*].properties[*].properties[*]
      - $.components.schemas[*].properties[*].items.properties[*]
      - $.components.schemas[*].properties[*].allOf[*].properties[*]
      - $.paths[*][get,put,post,delete,options,head,patch,trace,query].requestBody.content[*].schema.properties[*]
      - $.paths[*][get,put,post,delete,options,head,patch,trace,query].responses[*].content[*].schema.properties[*]
    then:
      function: schema
      functionOptions:
        schema:
          type: object
          anyOf:
            - required: [type]
            - required: [$ref]
            - required: [allOf]
            - required: [oneOf]
            - required: [anyOf]
  property-name-snake-case:
    description: Schema properties are snake_case. `baseURL` and `humanURL` are exempt by name — they are APIs.json vocabulary, the wire format the catalog is built on, and not this API's to rename.
    severity: warn
    given:
      - $.components.schemas[*].properties
      - $.components.schemas[*].properties[*].properties
      - $.components.schemas[*].properties[*].items.properties
      - $.components.schemas[*].properties[*].allOf[*].properties
      - $.paths[*][get,put,post,delete,options,head,patch,trace,query].requestBody.content[*].schema.properties
    then:
      field: '@key'
      function: pattern
      functionOptions:
        match: ^([a-z][a-z0-9_]*|baseURL|humanURL)$
  property-scalar-carries-example:
    description: Every scalar property shows a real value — examples are what agents and readers reach for first.
    severity: warn
    given:
      - $.components.schemas[*].properties[*]
      - $.components.schemas[*].properties[*].properties[*]
      - $.components.schemas[*].properties[*].items.properties[*]
      - $.components.schemas[*].properties[*].allOf[*].properties[*]
      - $.paths[*][get,put,post,delete,options,head,patch,trace,query].requestBody.content[*].schema.properties[*]
    then:
      function: schema
      functionOptions:
        schema:
          type: object
          if:
            required: [type]
            properties:
              type:
                enum: [string, integer, number]
            not:
              anyOf:
                - required: [enum]
                - required: [default]
          then:
            anyOf:
              - required: [examples]
              - required: [example]
              - required: [format]
  property-string-bounded:
    description: Every string property leaves the API bounded — maxLength, an enum, or a closed format.
    severity: warn
    given:
      - $.components.schemas[*].properties[*]
      - $.components.schemas[*].properties[*].properties[*]
      - $.components.schemas[*].properties[*].items.properties[*]
      - $.components.schemas[*].properties[*].allOf[*].properties[*]
      - $.paths[*][get,put,post,delete,options,head,patch,trace,query].requestBody.content[*].schema.properties[*]
    then:
      function: schema
      functionOptions:
        schema:
          type: object
          if:
            required: [type]
            properties:
              type:
                const: string
          then:
            anyOf:
              - required: [maxLength]
              - required: [enum]
              - properties:
                  format:
                    enum: [date, date-time]
                required: [format]

  # --------------------------------------------------------- security schemes
  security-scheme-description-required:
    description: Every security scheme explains how keys behave — including that gated calls answer 402, not a refusal.
    severity: error
    given: $.components.securitySchemes[*]
    then:
      field: description
      function: truthy
  security-scheme-shape:
    description: The APIs.io scheme is an apiKey in the X-API-Key header.
    severity: error
    given: $.components.securitySchemes[*]
    then:
      function: schema
      functionOptions:
        schema:
          type: object
          required: [type, in, name]
          properties:
            type:
              const: apiKey
            in:
              const: header
            name:
              const: X-API-Key

Work with this as data

Every ruleset 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 spectral rules

4 MCP tools reach this
  • find_rulesBrowse and filter every ruleset in the catalog.
  • 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 ruleset
curl "https://apis.io/api/v1/rules/apis-io-spectral-rules"
All spectral rules
curl "https://apis.io/api/v1/rules?limit=25"

Discovery needs no key. Ratings and market analysis are Pro.

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.