# 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