API Evangelist · API Governance Rules

API Evangelist API Rules

Spectral linting rules defining API design standards and conventions for API Evangelist.

47 Rules error 8 warn 31 info 8
Generated by API Evangelist Written by API Evangelist tooling for API Evangelist's APIs. It is a starting point, not a ruleset API Evangelist publishes or enforces.
View Rules File View on GitHub

Rule Categories

apikey components error info no openapi operation pagination parameter path query response schema security servers tag

Rules

error
openapi-version-supported
The document must declare OpenAPI 3.1 or 3.2. Both contracts are 3.1.0.
$
warn
info-contact-complete
info.contact must carry a name, an email and a URL. A contract that cannot be asked a question about itself is a dead end for whoever inherits the integration.
$.info
warn
info-license-present
info.license must say what the consumer may do with the contract and the data behind it. Measured: the v1 contract declares CC BY-SA 4.0; governance declares none.
$.info
info
info-summary-present
info.summary gives an agent one line to route on without parsing the whole description. Measured: present on v1, absent on governance.
$.info
warn
info-description-substantive
info.description must be a real orientation, not a restated title -- at least 200 characters covering what the API is for and how access works.
$.info.description
warn
info-version-semver
info.version must be semantic (MAJOR.MINOR.PATCH). Measured 1.2.0 and 1.0.0.
$.info
error
servers-defined
The document must declare at least one server, or the contract is not callable.
$
error
servers-https-only
Server URLs must use HTTPS. Measured 100% already compliant.
$.servers[*].url
warn
servers-expected-domain
Server URLs must be on apievangelist.com. A contract published under this provider that points somewhere else is either mis-filed or a copied template.
$.servers[*].url
warn
servers-no-trailing-slash
Server URLs must not end in a slash -- concatenating one with a path that also starts with a slash is the single most common cause of a 404 in a generated client.
$.servers[*].url
warn
path-segments-snake-case
Path segments and path parameters must be snake_case. Measured snake @ 97% (n=144); the 4 exceptions are all in the governance contract -- agent-descriptor, agent-export, context-gate, pipeline-audit.
$.paths
warn
path-no-trailing-slash
Paths must not end in a slash (the root path excepted). A contract that documents both /x and /x/ documents two resources it does not have.
$.paths
warn
path-no-file-extension
Paths must not carry a file extension. Format is a matter for Accept or an explicit `format` parameter, not for the resource identifier.
$.paths
warn
path-params-snake-case
Path parameter names must be snake_case. Measured snake @ 100% (n=25).
$.paths[*].parameters[?(@.in=='path')].name
error
operation-operationid-present
Every operation must carry an operationId. It is the name every generated client method and every MCP tool derives from; without it the generator invents one.
$.paths[*][get,put,post,delete,patch,head,options]
warn
operation-operationid-camel-case
operationIds must be camelCase. Measured camel @ 100% (n=92) -- the rule holds the line rather than reporting a backlog.
$.paths[*][get,put,post,delete,patch,head,options].operationId
warn
operation-summary-present
One line saying what the operation does. Measured 92/92 already compliant.
$.paths[*][get,put,post,delete,patch,head,options]
info
operation-summary-concise
A summary is a label, not a paragraph -- keep it under 120 characters and put the rest in the description, which is where a reference renderer expects it.
$.paths[*][get,put,post,delete,patch,head,options].summary
warn
operation-description-present
Every operation must describe itself beyond its summary -- what it returns, what it costs, and which tier it needs.
$.paths[*][get,put,post,delete,patch,head,options]
warn
operation-tags-present
Every operation must be tagged. Tags are the navigation of the reference and the split key for every per-resource artifact derived from this contract.
$.paths[*][get,put,post,delete,patch,head,options]
warn
operation-single-tag
Exactly one tag per operation. Measured: 0 of 92 operations carry more than one, and a second tag silently duplicates the operation across split artifacts.
$.paths[*][get,put,post,delete,patch,head,options].tags
warn
operation-deprecated-needs-sunset
A deprecated operation must declare `x-sunset` with the date it goes away. Deprecation without a date is a warning nobody can plan against, and the published deprecation policy commits to six months' notice.
$.paths[*][get,put,post,delete,patch,head,options]
warn
query-params-snake-case
Query parameter names must be snake_case. Measured snake @ 100% (n=175).
$.paths[*][get,put,post,delete,patch,head,options].parameters[?(@.in=='query')].name
warn
components-parameters-pascal-case
Reusable parameter component KEYS must be PascalCase, so a $ref reads as a type name while the wire-level `name` stays snake_case. Measured pascal @ 100% (n=9).
$.components.parameters
info
parameter-description-present
Every parameter must say what it does. Measured: 6 distinct parameters carry no description, including `limit` and `page`.
$.components.parameters[*]$.paths[*][get,put,post,delete,patch,head,options].parameters[?(@.name)]
warn
parameter-schema-typed
Every parameter must declare a schema. An untyped parameter is a string by accident, and every generated client will treat it as one.
$.components.parameters[*]$.paths[*][get,put,post,delete,patch,head,options].parameters[?(@.name)]
warn
pagination-limit-bounded
A `limit` parameter must declare a maximum, or the contract promises a page size the service will refuse. Measured: components.parameters.Limit is correctly bounded 1..100 with a default of 25.
$.components.parameters[?(@.name == 'limit')]$.paths[*][get,put,post,delete,patch,head,options].parameters[?(@.name == 'limit')]
warn
response-2xx-present
Every operation must document a success response. Measured: 91 of 92 do -- /auth/login documents only a 302, which is a real gap for any non-browser client.
$.paths[*][get,put,post,delete,patch,head,options]
warn
response-error-documented
Every operation must document at least one failure. Measured: only 48 of 92 do, so two thirds of this API's error behaviour is undocumented.
$.paths[*][get,put,post,delete,patch,head,options]
warn
response-description-present
Every response must describe the condition it represents.
$.paths[*][get,put,post,delete,patch,head,options].responses[*]
info
response-json-media-type
Responses that carry a body must offer application/json. Measured: 25 of 26 media types are application/json; the exception is the text/html sign-in redirect.
$.paths[*][get,put,post,delete,patch,head,options].responses[*].content
info
response-example-present
Response bodies should carry an example or examples object. Agents and generated SDKs read examples as the shape contract; a schema alone leaves every consumer guessing at real values. Measured 63/66 on v1, 0/26 on governance.
$.paths[*][get,put,post,delete,patch,head,options].responses[*].content[*]
info
response-headers-documented
Successful responses should document their headers. Rate-limit and pagination signals are part of the contract, and an agent that cannot see them cannot pace itself. Measured: 0 of 92 operations document any response header.
$.paths[*][get,put,post,delete,patch,head,options].responses[?(@property.match(/^2/))]
warn
error-response-shares-a-schema
A 4xx or 5xx body must $ref a shared error schema rather than redefining the shape inline, so one error contract covers the whole API.
$.paths[*][get,put,post,delete,patch,head,options].responses[?(@property.match(/^[45]/))].content[*]
warn
error-schema-defined
A shared `Error` schema must exist in components. Anchored at the document root for the same reason as security-schemes-defined: the governance contract defines no components.schemas whatsoever, so a rule given $.components.schemas selected nothing and reported it clean. Measured: defined in v1, absent from governance -- which is why that contract has no error shape to $ref.
$
warn
schema-names-pascal-case
Component schema names must be PascalCase. Measured pascal @ 100% (n=7).
$.components.schemas
warn
schema-properties-snake-case
Schema properties must be snake_case. Measured snake @ 100% (n=20).
$.components.schemas[*].properties
warn
schema-has-type
Every component schema must declare a type. Measured: 5 of 7 do. An untyped schema validates nothing and generates nothing.
$.components.schemas[*]
info
schema-has-description
Every component schema must describe the thing it models. Measured: 1 of 7 do.
$.components.schemas[*]
info
schema-property-descriptions
Every schema property should describe what it holds -- property names carry less meaning to a consumer than the author assumes. Measured: 17 of 20 have none.
$.components.schemas[*].properties[*]
warn
tag-has-description
Declared tags must have descriptions -- they are the navigation of the reference and of every tag-split artifact derived from it. Measured 21/21 compliant.
$.tags[*]
error
security-schemes-defined
Security schemes must be defined in components. Anchored at the document root, not at $.components -- a document with no components object at all selects nothing under $.components, and the rule would go silent on exactly the case it exists to catch. Measured: both contracts declare one.
$
warn
security-applied-at-root
A root-level `security` array must state the default posture for every operation, including the keyless tier, rather than leaving it to be inferred.
$
error
apikey-scheme-in-header
An apiKey scheme must be carried in a header or a cookie -- never a query string, where the credential lands in access logs, referrers and browser history.
$.components.securitySchemes[*]
warn
apikey-header-name-consistent
Header-borne API keys must use `x-api-key` across the estate. Measured: both contracts already do, and a client configured for one should work against the other.
$.components.securitySchemes[?(@.type == 'apiKey' && @.in == 'header')]
error
no-credentials-in-query
No parameter may carry a credential in the query string. Matches the common names -- api_key, apikey, token, access_token, password, secret.
$.components.parameters[?(@.in == 'query')].name$.paths[*][get,put,post,delete,patch,head,options].parameters[?(@.in == 'query')].name
error
no-credentials-in-path
No path parameter may carry a credential. A secret in a path is a secret in every proxy log and every shared URL.
$.paths[*].parameters[?(@.in == 'path')].name

Spectral Ruleset

Raw ↑
# Spectral validates a ruleset against a CLOSED schema -- a root `method:` key
# makes the file fail to load ("must NOT have additional properties"), i.e. a
# stamped ruleset is one Spectral will not run. `x-method:` carries the same
# provenance and loads. Verified with @stoplight/spectral-cli 2026-08-20.
x-method: generated
x-generated: '2026-08-20'
x-source:
  - openapi/apievangelist-v1-openapi.yml
  - openapi/apievangelist-governance-openapi.json

# =============================================================================
# api-evangelist -- Spectral ruleset
#
# Regenerated 2026-08-20 from the provider's own two contracts. Every convention
# below was MEASURED across all 92 operations before it was written down, and the
# measurement is recorded beside the rule. Where the two contracts disagree the
# dominant convention wins and the rule fires on the minority -- that is the
# point of a house style, and a rule tuned until it stops firing measures nothing.
#
# Measured (v1 = 66 ops, governance = 26 ops):
#   openapi                3.1.0 on both
#   servers                1 each, https, api.apievangelist.com/v1, no trailing slash
#   path segments          snake @ 97% (n=144) -- 4 kebab exceptions, all governance
#   path parameters        snake @ 100% (n=25)
#   operationIds           camel @ 100% (n=92)
#   query parameters       snake @ 100% (n=175, refs resolved)
#   components.parameters  PascalCase @ 100% (n=9)
#   component schemas      PascalCase @ 100% (n=7); properties snake @ 100% (n=20)
#   auth                   apiKey in header, `x-api-key`, on both; root-level security
#   media type             application/json (one text/html redirect on /auth/login)
#   shared error schema    `Error` (v1); governance defines no component schemas
#   response examples      63/66 v1, 0/26 governance
#   response headers       0 of 92 operations document any
#
# Severity is chosen so the ruleset is usable in a build:
#   error -- unambiguously wrong or unsafe; should block
#   warn  -- house convention broken; should be seen
#   info  -- documentation completeness; advisory
#
# Built-in Spectral functions only. No custom-function bundle to ship alongside.
# =============================================================================
extends:
  - spectral:oas

rules:

  # ---------------------------------------------------------------------------
  # Document & metadata
  # ---------------------------------------------------------------------------
  openapi-version-supported:
    description: The document must declare OpenAPI 3.1 or 3.2. Both contracts are 3.1.0.
    severity: error
    given: $
    then:
      field: openapi
      function: pattern
      functionOptions:
        match: ^3\.[12](\.\d+)?$

  info-contact-complete:
    description: >-
      info.contact must carry a name, an email and a URL. A contract that cannot be
      asked a question about itself is a dead end for whoever inherits the integration.
    severity: warn
    given: $.info
    then:
      field: contact
      function: schema
      functionOptions:
        schema:
          type: object
          required: [name, email, url]

  info-license-present:
    description: >-
      info.license must say what the consumer may do with the contract and the data
      behind it. Measured: the v1 contract declares CC BY-SA 4.0; governance declares none.
    severity: warn
    given: $.info
    then:
      field: license
      function: truthy

  info-summary-present:
    description: >-
      info.summary gives an agent one line to route on without parsing the whole
      description. Measured: present on v1, absent on governance.
    severity: info
    given: $.info
    then:
      field: summary
      function: truthy

  info-description-substantive:
    description: >-
      info.description must be a real orientation, not a restated title -- at least
      200 characters covering what the API is for and how access works.
    severity: warn
    given: $.info.description
    then:
      function: length
      functionOptions:
        min: 200

  info-version-semver:
    description: info.version must be semantic (MAJOR.MINOR.PATCH). Measured 1.2.0 and 1.0.0.
    severity: warn
    given: $.info
    then:
      field: version
      function: pattern
      functionOptions:
        match: ^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$

  # ---------------------------------------------------------------------------
  # Servers
  # ---------------------------------------------------------------------------
  servers-defined:
    description: The document must declare at least one server, or the contract is not callable.
    severity: error
    given: $
    then:
      field: servers
      function: truthy

  servers-https-only:
    description: Server URLs must use HTTPS. Measured 100% already compliant.
    severity: error
    given: $.servers[*].url
    then:
      function: pattern
      functionOptions:
        match: ^https://

  servers-expected-domain:
    description: >-
      Server URLs must be on apievangelist.com. A contract published under this
      provider that points somewhere else is either mis-filed or a copied template.
    severity: warn
    given: $.servers[*].url
    then:
      function: pattern
      functionOptions:
        match: ^https://[a-z0-9.-]*apievangelist\.com(/|$)

  servers-no-trailing-slash:
    description: >-
      Server URLs must not end in a slash -- concatenating one with a path that also
      starts with a slash is the single most common cause of a 404 in a generated client.
    severity: warn
    given: $.servers[*].url
    then:
      function: pattern
      functionOptions:
        notMatch: /$

  # ---------------------------------------------------------------------------
  # Paths
  # ---------------------------------------------------------------------------
  path-segments-snake-case:
    description: >-
      Path segments and path parameters must be snake_case. Measured snake @ 97%
      (n=144); the 4 exceptions are all in the governance contract --
      agent-descriptor, agent-export, context-gate, pipeline-audit.
    severity: warn
    given: $.paths
    then:
      field: '@key'
      function: pattern
      functionOptions:
        match: ^/$|^(/([a-z0-9]+(_[a-z0-9]+)*|\{[a-z0-9]+(_[a-z0-9]+)*\}))+$

  path-no-trailing-slash:
    description: >-
      Paths must not end in a slash (the root path excepted). A contract that
      documents both /x and /x/ documents two resources it does not have.
    severity: warn
    given: $.paths
    then:
      field: '@key'
      function: pattern
      functionOptions:
        notMatch: .+/$

  path-no-file-extension:
    description: >-
      Paths must not carry a file extension. Format is a matter for Accept or an
      explicit `format` parameter, not for the resource identifier.
    severity: warn
    given: $.paths
    then:
      field: '@key'
      function: pattern
      functionOptions:
        notMatch: \.[A-Za-z0-9]+$

  path-params-snake-case:
    description: Path parameter names must be snake_case. Measured snake @ 100% (n=25).
    severity: warn
    given: $.paths[*].parameters[?(@.in=='path')].name
    then:
      function: casing
      functionOptions:
        type: snake

  # ---------------------------------------------------------------------------
  # Operations
  # ---------------------------------------------------------------------------
  operation-operationid-present:
    description: >-
      Every operation must carry an operationId. It is the name every generated
      client method and every MCP tool derives from; without it the generator invents one.
    severity: error
    given: $.paths[*][get,put,post,delete,patch,head,options]
    then:
      field: operationId
      function: truthy

  operation-operationid-camel-case:
    description: >-
      operationIds must be camelCase. Measured camel @ 100% (n=92) -- the rule
      holds the line rather than reporting a backlog.
    severity: warn
    given: $.paths[*][get,put,post,delete,patch,head,options].operationId
    then:
      function: casing
      functionOptions:
        type: camel

  operation-summary-present:
    description: One line saying what the operation does. Measured 92/92 already compliant.
    severity: warn
    given: $.paths[*][get,put,post,delete,patch,head,options]
    then:
      field: summary
      function: truthy

  operation-summary-concise:
    description: >-
      A summary is a label, not a paragraph -- keep it under 120 characters and put
      the rest in the description, which is where a reference renderer expects it.
    severity: info
    given: $.paths[*][get,put,post,delete,patch,head,options].summary
    then:
      function: length
      functionOptions:
        max: 120

  operation-description-present:
    description: >-
      Every operation must describe itself beyond its summary -- what it returns,
      what it costs, and which tier it needs.
    severity: warn
    given: $.paths[*][get,put,post,delete,patch,head,options]
    then:
      field: description
      function: truthy

  operation-tags-present:
    description: >-
      Every operation must be tagged. Tags are the navigation of the reference and
      the split key for every per-resource artifact derived from this contract.
    severity: warn
    given: $.paths[*][get,put,post,delete,patch,head,options]
    then:
      field: tags
      function: truthy

  operation-single-tag:
    description: >-
      Exactly one tag per operation. Measured: 0 of 92 operations carry more than
      one, and a second tag silently duplicates the operation across split artifacts.
    severity: warn
    given: $.paths[*][get,put,post,delete,patch,head,options].tags
    then:
      function: length
      functionOptions:
        max: 1

  operation-deprecated-needs-sunset:
    description: >-
      A deprecated operation must declare `x-sunset` with the date it goes away.
      Deprecation without a date is a warning nobody can plan against, and the
      published deprecation policy commits to six months' notice.
    severity: warn
    given: $.paths[*][get,put,post,delete,patch,head,options]
    then:
      function: schema
      functionOptions:
        schema:
          type: object
          if:
            type: object
            properties:
              deprecated:
                const: true
            required: [deprecated]
          then:
            type: object
            required: [x-sunset]

  # ---------------------------------------------------------------------------
  # Parameters
  # ---------------------------------------------------------------------------
  query-params-snake-case:
    description: Query parameter names must be snake_case. Measured snake @ 100% (n=175).
    severity: warn
    given: $.paths[*][get,put,post,delete,patch,head,options].parameters[?(@.in=='query')].name
    then:
      function: casing
      functionOptions:
        type: snake

  components-parameters-pascal-case:
    description: >-
      Reusable parameter component KEYS must be PascalCase, so a $ref reads as a
      type name while the wire-level `name` stays snake_case. Measured pascal @ 100% (n=9).
    severity: warn
    given: $.components.parameters
    then:
      field: '@key'
      function: casing
      functionOptions:
        type: pascal

  parameter-description-present:
    description: >-
      Every parameter must say what it does. Measured: 6 distinct parameters carry
      no description, including `limit` and `page`.
    severity: info
    resolved: false
    given:
      - $.components.parameters[*]
      - $.paths[*][get,put,post,delete,patch,head,options].parameters[?(@.name)]
    then:
      field: description
      function: truthy

  parameter-schema-typed:
    description: >-
      Every parameter must declare a schema. An untyped parameter is a string by
      accident, and every generated client will treat it as one.
    severity: warn
    resolved: false
    given:
      - $.components.parameters[*]
      - $.paths[*][get,put,post,delete,patch,head,options].parameters[?(@.name)]
    then:
      field: schema
      function: truthy

  pagination-limit-bounded:
    description: >-
      A `limit` parameter must declare a maximum, or the contract promises a page
      size the service will refuse. Measured: components.parameters.Limit is
      correctly bounded 1..100 with a default of 25.
    severity: warn
    resolved: false
    given:
      - $.components.parameters[?(@.name == 'limit')]
      - $.paths[*][get,put,post,delete,patch,head,options].parameters[?(@.name == 'limit')]
    then:
      function: schema
      functionOptions:
        schema:
          type: object
          required: [schema]
          properties:
            schema:
              type: object
              required: [maximum]

  # ---------------------------------------------------------------------------
  # Responses
  # ---------------------------------------------------------------------------
  response-2xx-present:
    description: >-
      Every operation must document a success response. Measured: 91 of 92 do --
      /auth/login documents only a 302, which is a real gap for any non-browser client.
    severity: warn
    given: $.paths[*][get,put,post,delete,patch,head,options]
    then:
      field: responses
      function: schema
      functionOptions:
        schema:
          type: object
          anyOf:
            - required: ['200']
            - required: ['201']
            - required: ['202']
            - required: ['204']

  response-error-documented:
    description: >-
      Every operation must document at least one failure. Measured: only 48 of 92
      do, so two thirds of this API's error behaviour is undocumented.
    severity: warn
    given: $.paths[*][get,put,post,delete,patch,head,options]
    then:
      field: responses
      function: schema
      functionOptions:
        schema:
          type: object
          anyOf:
            - required: ['400']
            - required: ['401']
            - required: ['402']
            - required: ['403']
            - required: ['404']
            - required: ['422']
            - required: ['429']
            - required: ['500']

  response-description-present:
    description: Every response must describe the condition it represents.
    severity: warn
    given: $.paths[*][get,put,post,delete,patch,head,options].responses[*]
    then:
      field: description
      function: truthy

  response-json-media-type:
    description: >-
      Responses that carry a body must offer application/json. Measured: 25 of 26
      media types are application/json; the exception is the text/html sign-in redirect.
    severity: info
    given: $.paths[*][get,put,post,delete,patch,head,options].responses[*].content
    then:
      field: application/json
      function: truthy

  response-example-present:
    description: >-
      Response bodies should carry an example or examples object. Agents and
      generated SDKs read examples as the shape contract; a schema alone leaves
      every consumer guessing at real values. Measured 63/66 on v1, 0/26 on governance.
    severity: info
    given: $.paths[*][get,put,post,delete,patch,head,options].responses[*].content[*]
    then:
      function: schema
      functionOptions:
        schema:
          type: object
          anyOf:
            - required: [example]
            - required: [examples]

  response-headers-documented:
    description: >-
      Successful responses should document their headers. Rate-limit and pagination
      signals are part of the contract, and an agent that cannot see them cannot
      pace itself. Measured: 0 of 92 operations document any response header.
    severity: info
    given: $.paths[*][get,put,post,delete,patch,head,options].responses[?(@property.match(/^2/))]
    then:
      field: headers
      function: truthy

  error-response-shares-a-schema:
    description: >-
      A 4xx or 5xx body must $ref a shared error schema rather than redefining the
      shape inline, so one error contract covers the whole API.
    severity: warn
    resolved: false
    given: $.paths[*][get,put,post,delete,patch,head,options].responses[?(@property.match(/^[45]/))].content[*]
    then:
      field: schema
      function: schema
      functionOptions:
        schema:
          type: object
          required: ['$ref']

  # ---------------------------------------------------------------------------
  # Schemas
  # ---------------------------------------------------------------------------
  error-schema-defined:
    description: >-
      A shared `Error` schema must exist in components. Anchored at the document
      root for the same reason as security-schemes-defined: the governance
      contract defines no components.schemas whatsoever, so a rule given
      $.components.schemas selected nothing and reported it clean. Measured:
      defined in v1, absent from governance -- which is why that contract has no
      error shape to $ref.
    severity: warn
    given: $
    then:
      function: schema
      functionOptions:
        schema:
          type: object
          required: [components]
          properties:
            components:
              type: object
              required: [schemas]
              properties:
                schemas:
                  type: object
                  required: [Error]

  schema-names-pascal-case:
    description: Component schema names must be PascalCase. Measured pascal @ 100% (n=7).
    severity: warn
    given: $.components.schemas
    then:
      field: '@key'
      function: casing
      functionOptions:
        type: pascal

  schema-properties-snake-case:
    description: Schema properties must be snake_case. Measured snake @ 100% (n=20).
    severity: warn
    given: $.components.schemas[*].properties
    then:
      field: '@key'
      function: casing
      functionOptions:
        type: snake

  schema-has-type:
    description: >-
      Every component schema must declare a type. Measured: 5 of 7 do. An untyped
      schema validates nothing and generates nothing.
    severity: warn
    given: $.components.schemas[*]
    then:
      field: type
      function: truthy

  schema-has-description:
    description: >-
      Every component schema must describe the thing it models. Measured: 1 of 7 do.
    severity: info
    given: $.components.schemas[*]
    then:
      field: description
      function: truthy

  schema-property-descriptions:
    description: >-
      Every schema property should describe what it holds -- property names carry
      less meaning to a consumer than the author assumes. Measured: 17 of 20 have none.
    severity: info
    given: $.components.schemas[*].properties[*]
    then:
      field: description
      function: truthy

  # ---------------------------------------------------------------------------
  # Tags
  # ---------------------------------------------------------------------------
  tag-has-description:
    description: >-
      Declared tags must have descriptions -- they are the navigation of the
      reference and of every tag-split artifact derived from it. Measured 21/21 compliant.
    severity: warn
    given: $.tags[*]
    then:
      field: description
      function: truthy

  # ---------------------------------------------------------------------------
  # Security
  # ---------------------------------------------------------------------------
  security-schemes-defined:
    description: >-
      Security schemes must be defined in components. Anchored at the document
      root, not at $.components -- a document with no components object at all
      selects nothing under $.components, and the rule would go silent on exactly
      the case it exists to catch. Measured: both contracts declare one.
    severity: error
    given: $
    then:
      function: schema
      functionOptions:
        schema:
          type: object
          required: [components]
          properties:
            components:
              type: object
              required: [securitySchemes]

  security-applied-at-root:
    description: >-
      A root-level `security` array must state the default posture for every
      operation, including the keyless tier, rather than leaving it to be inferred.
    severity: warn
    given: $
    then:
      field: security
      function: truthy

  apikey-scheme-in-header:
    description: >-
      An apiKey scheme must be carried in a header or a cookie -- never a query
      string, where the credential lands in access logs, referrers and browser history.
    severity: error
    given: $.components.securitySchemes[*]
    then:
      function: schema
      functionOptions:
        schema:
          type: object
          if:
            type: object
            properties:
              type:
                const: apiKey
            required: [type]
          then:
            type: object
            properties:
              in:
                enum: [header, cookie]

  apikey-header-name-consistent:
    description: >-
      Header-borne API keys must use `x-api-key` across the estate. Measured: both
      contracts already do, and a client configured for one should work against the other.
    severity: warn
    given: $.components.securitySchemes[?(@.type == 'apiKey' && @.in == 'header')]
    then:
      field: name
      function: pattern
      functionOptions:
        match: ^x-api-key$

  no-credentials-in-query:
    description: >-
      No parameter may carry a credential in the query string. Matches the common
      names -- api_key, apikey, token, access_token, password, secret.
    severity: error
    resolved: false
    given:
      - $.components.parameters[?(@.in == 'query')].name
      - $.paths[*][get,put,post,delete,patch,head,options].parameters[?(@.in == 'query')].name
    then:
      function: pattern
      functionOptions:
        # Spectral's pattern function compiles JS regex — inline (?i) is a syntax
        # error there. Flags go in the /…/i literal form instead.
        notMatch: /^(api[-_]?key|access[-_]?token|auth[-_]?token|token|secret|password|passwd|pwd|client[-_]?secret)$/i

  no-credentials-in-path:
    description: >-
      No path parameter may carry a credential. A secret in a path is a secret in
      every proxy log and every shared URL.
    severity: error
    resolved: false
    given: $.paths[*].parameters[?(@.in == 'path')].name
    then:
      function: pattern
      functionOptions:
        # Spectral's pattern function compiles JS regex — inline (?i) is a syntax
        # error there. Flags go in the /…/i literal form instead.
        notMatch: /^(api[-_]?key|access[-_]?token|auth[-_]?token|token|secret|password|passwd|pwd|client[-_]?secret)$/i

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/api-evangelist-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.