Camunda · API Governance Rules

Camunda — camunda rules

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

30 Rules error 28 warn 2
Published by Camunda Served by the provider at https://github.com/camunda/camunda/blob/bb9c4b0c35d450622e77f151a814bf3c1c6a27e8/zeebe/gateway-protocol/.spectral.yaml; the copy below was fetched from there.
View Rules File View on GitHub

Rule Categories

added array no operation permission present properties require required response schema semantic valid verify

Rules

error
no-period-in-summary
Path summary must not include period
$.paths[*][*].summary
error
no-flow-node-in-path-summary
Path summary must not include term "flow node"
$.paths[*][*].summary
error
no-flow-node-in-path-description
Path description must not include term "flow node"
$.paths[*][*].description
error
no-flow-node-in-operation-id
Path operation ID must not include term "flow node"
$.paths[*][*].operationId
error
no-flow-node-in-path
Paths must not include term "flow node"
$.paths.*~
error
no-flow-node-in-component-property
Component properties must not include term "flow node"
$.components[*][*].properties.*~
error
require-property-descriptions
Schema properties must have a description.
$.components.schemas[*].properties[*]
warn
operation-key-properties-must-be-strings
`...Key` parameters must be of type `string`.
$.paths[*][*].parameters[*]
warn
schema-key-properties-must-be-strings
`...Key` properties must be of type `string`.
$.components.schemas[*].properties
error
required-properties-must-exist
Every entry in `required` must match a key in `properties`.
$.components.schemas[*]
error
no-redundant-required
A schema's own `required` list must not duplicate a field already required by an allOf-composed schema.
$.components.schemas[*]
error
array-properties-must-be-required
Response array properties must be `required` and not `nullable`.
$.paths[*]
error
response-properties-must-be-required
All response body properties must be listed in `required`.
$.paths[*]
error
no-eventually-consistent-on-commands
Command (mutating) operations must not have x-eventually-consistent: true.
$.paths[*][*]
error
valid-deprecated-enum-members
`x-deprecated-enum-members` must be a valid array of {name, deprecatedInVersion} referencing existing enum values.
$..x-deprecated-enum-members^
error
require-added-in-version
Every operation must have `x-added-in-version` declaring the Camunda version in which it was introduced.
$.paths[*][*]
error
require-scope
Every operation must have a valid `x-scope` (`cluster-wide` or `physical-tenant`).
$.paths[*][*]
error
present-when-shape
Each `x-present-when` marker must declare a request field and a scalar `equals` value.
$..x-present-when
error
present-when-request-resolves
Each `x-present-when.request` must name a top-level property of its operation's request body, and the annotated response property must stay nullable and required.
$.paths[*]
error
required-permissions-shape
Each `x-required-permissions` entry must conform to the documented schema.
$.paths[*][get,put,post,patch,delete].x-required-permissions[*]
error
verify-required-permissions
Every operation must declare `x-required-permissions` (or `[]` if unrestricted), and each static `{resourceType, permissionType}` pair must be valid per resource-permissions.json.
$
error
permission-enforcement-shape
`x-permission-enforcement` must be either `reject` or `filter`.
$.paths[*][get,put,post,patch,delete].x-permission-enforcement
error
semantic-establishes-shape
`x-semantic-establishes` must conform to the documented schema.
$.paths[*][get,put,post,patch,delete].x-semantic-establishes
error
semantic-requires-shape
`x-semantic-requires` must conform to the documented schema.
$.paths[*][get,put,post,patch,delete].x-semantic-requires
error
verify-semantic-kinds-registered
Every `kind:` referenced by x-semantic-establishes / x-semantic-requires must appear in semantic-kinds.json, every required kind must be established somewhere in the spec, and every identifiedBy/bind member must reference a parameter or top-level requestBody property that actually exists on the operation.
$
error
properties-added-in-version-shape
`x-properties-added-in-version` must be a non-empty array of objects, each requiring `propertyName` plus at least one of `addedInVersion`/`addedInVersionOverride`, and no other keys.
$..x-properties-added-in-version
error
added-in-version-override-shape
`x-added-in-version-override`, when present on an operation, must be a version string.
$.paths[*][*].x-added-in-version-override
error
no-ambiguous-identifier-property
Schema properties must not use bare `id` or `key` — use qualified names like `entityNameId`.
$.components.schemas[*]
error
no-ambiguous-identifier-property-inline
Inline schema properties must not use bare `id` or `key` — use qualified names like `entityNameId`.
$.paths[*][*].requestBody.content[*].schema$.paths[*][*].responses[*].content[*].schema$.paths[*][*].parameters[*].schema
error
require-security-declared
Every operation must declare a `security:` block.
$.paths[*][*]

Spectral Ruleset

Raw ↑
# harvested from https://github.com/camunda/camunda/blob/bb9c4b0c35d450622e77f151a814bf3c1c6a27e8/zeebe/gateway-protocol/.spectral.yaml on 2026-10-09 — a Spectral ruleset published in the provider's own GitHub repository (camunda/camunda); found by GitHub code search, fetched verbatim
x-method: harvested
x-stamped: 2026-10-09
x-source-url: https://github.com/camunda/camunda/blob/bb9c4b0c35d450622e77f151a814bf3c1c6a27e8/zeebe/gateway-protocol/.spectral.yaml
# Spectral ruleset for OpenAPI validation
# Replaces Vacuum for comprehensive OpenAPI linting and validation
description: Recommended ruleset with additional customizations.
extends: [[spectral:oas, recommended]]
ignoreUnknownFormat: true
functions:
  - verifyKeyTypesPaths
  - verifyKeyTypesSchemas
  - verifyEventuallyConsistent
  - verifyRequiredPropertiesExist
  - verifyNoRedundantRequired
  - verifyArrayPropertiesRequired
  - verifyResponsePropertiesRequired
  - verifyDeprecatedEnumMembers
  - verifyAddedInVersion
  - verifyScope
  - verifySemanticKindsRegistered
  - verifyPresentWhenRequestResolves
  - verifyNoAmbiguousIdentifierProperty
  - verifySecurityDeclared
  - verifyRequiredPermissions
functionsDir: ./spectral-functions
rules:
  # Tag validation
  operation-tag-defined: error

  # Schema example validation — @camunda8/spectral-cli supports unicodeRegExp,
  # so false-positive pattern failures are resolved.
  # oas3-valid-schema-example remains at warn because ScopeKey uses oneOf with
  # structurally identical branches (ProcessInstanceKey, ElementInstanceKey),
  # causing "must match exactly one schema in oneOf" on any ScopeKey example.
  # Changing oneOf→anyOf would fix the warning but breaks Java code generation.
  oas3-valid-schema-example: warn
  oas3-valid-media-example: error

  # Custom validation rules migrated from Vacuum
  no-period-in-summary:
    description: Path summary must not include period
    severity: error
    given: $.paths[*][*].summary
    then:
      function: pattern
      functionOptions:
        notMatch: ^.*\.$
    message: Summaries are short descriptions often used in navigation bars, they must not contain periods at the end. Please remove the period at the end of the summary text.

  no-flow-node-in-path-summary:
    description: Path summary must not include term "flow node"
    severity: error
    given: $.paths[*][*].summary
    then:
      function: pattern
      functionOptions:
        notMatch: .*(flow-node|flownode|flowNode|flow node).*
    message: Please replace the term "flow node" with "element".

  no-flow-node-in-path-description:
    description: Path description must not include term "flow node"
    severity: error
    given: $.paths[*][*].description
    then:
      function: pattern
      functionOptions:
        notMatch: .*(flow-node|flownode|flowNode|flow node).*
    message: Please replace the term "flow node" with "element".

  no-flow-node-in-operation-id:
    description: Path operation ID must not include term "flow node"
    severity: error
    given: $.paths[*][*].operationId
    then:
      function: pattern
      functionOptions:
        notMatch: .*(flow-node|flownode|flowNode|FlowNode|Flownode).*
    message: Please replace the term "flow node" with "element".

  no-flow-node-in-path:
    description: Paths must not include term "flow node"
    severity: error
    given: $.paths.*~
    then:
      function: pattern
      functionOptions:
        notMatch: .*(flow-node|flownode|flowNode).*
    message: Please replace the term "flow node" with "element".

  no-flow-node-in-component-property:
    description: Component properties must not include term "flow node"
    severity: error
    given: $.components[*][*].properties.*~
    then:
      function: pattern
      functionOptions:
        notMatch: .*(flownode|flowNode).*
    message: Please replace the term "flow node" with "element".

  require-property-descriptions:
    description: Schema properties must have a description.
    severity: error
    given: $.components.schemas[*].properties[*]
    then:
      field: description
      function: truthy

  operation-key-properties-must-be-strings:
    description: "`...Key` parameters must be of type `string`."
    severity: warn
    given: $.paths[*][*].parameters[*]
    then:
      function: verifyKeyTypesPaths
    message: Make the `...Key` property a `string`.

  schema-key-properties-must-be-strings:
    description: "`...Key` properties must be of type `string`."
    severity: warn
    given: $.components.schemas[*].properties
    then:
      function: verifyKeyTypesSchemas
    message: Make the `...Key` property a `string`.

  required-properties-must-exist:
    description: "Every entry in `required` must match a key in `properties`."
    severity: error
    given: "$.components.schemas[*]"
    then:
      function: verifyRequiredPropertiesExist

  no-redundant-required:
    description: "A schema's own `required` list must not duplicate a field already required by an allOf-composed schema."
    severity: error
    given: "$.components.schemas[*]"
    message: "{{error}}"
    then:
      function: verifyNoRedundantRequired

  array-properties-must-be-required:
    description: "Response array properties must be `required` and not `nullable`."
    severity: error
    given: "$.paths[*]"
    message: "{{error}}"
    then:
      function: verifyArrayPropertiesRequired

  response-properties-must-be-required:
    description: "All response body properties must be listed in `required`."
    severity: error
    given: "$.paths[*]"
    message: "{{error}}"
    then:
      function: verifyResponsePropertiesRequired

  no-eventually-consistent-on-commands:
    description: "Command (mutating) operations must not have x-eventually-consistent: true."
    severity: error
    given: $.paths[*][*]
    then:
      function: verifyEventuallyConsistent
    message: "Command operations are synchronous writes and must not be marked as eventually consistent. Only query operations (search, statistics, get) should use x-eventually-consistent: true."

  valid-deprecated-enum-members:
    description: "`x-deprecated-enum-members` must be a valid array of {name, deprecatedInVersion} referencing existing enum values."
    severity: error
    # Matches named schemas and inline (property-level) enums alike. resolved:
    # false reports at the schema's source location instead of at every $ref
    # consumer (mirrors properties-added-in-version-shape below). The parent
    # operator (^) is required over a [?(@['x-deprecated-enum-members'])]
    # filter: Spectral crashes walking into a `x-deprecated-enum-members:`
    # with no value (e.g. a YAML indentation mistake), since the filter
    # dereferences the property on every visited node, including once it is
    # null.
    resolved: false
    given: "$..x-deprecated-enum-members^"
    message: "{{error}}"
    then:
      function: verifyDeprecatedEnumMembers

  require-added-in-version:
    description: "Every operation must have `x-added-in-version` declaring the Camunda version in which it was introduced."
    severity: error
    given: $.paths[*][*]
    message: "{{error}}"
    then:
      function: verifyAddedInVersion

  # ── x-scope (camunda/camunda#61157) ──
  # Marks whether an operation is cluster-wide or scoped to a single physical
  # tenant, so the docs API reference can render a scope badge without
  # fragmenting the existing tag-based grouping. See
  # https://github.com/camunda/camunda-docs/issues/8886#issuecomment-5414815612.
  require-scope:
    description: "Every operation must have a valid `x-scope` (`cluster-wide` or `physical-tenant`)."
    severity: error
    given: $.paths[*][*]
    message: "{{error}}"
    then:
      function: verifyScope

  # ── x-present-when ──
  # Response-property marker declaring conditional presence: the annotated
  # response property is present (required, non-null) iff the named top-level
  # request-body field equals the given scalar; otherwise it is absent. This is
  # the ground-truth SDK generators derive request→response dependent typing
  # from (e.g. ActivatedJobResult.jobLeaseToken vs JobActivationRequest.withLease).
  # The marker is inert on the wire: the property keeps its declared nullable
  # shape for any consumer that does not derive from it.
  #
  # Scope: like the other `*-shape` rules here, this validates only the marker's
  # local JSON shape. It does NOT cross-reference that `request` names a real
  # top-level property on the operation's request body — that referential-
  # integrity check lives in the `present-when-request-resolves` rule below,
  # which resolves each operation against the real spec.
  present-when-shape:
    description: "Each `x-present-when` marker must declare a request field and a scalar `equals` value."
    severity: error
    given: $..x-present-when
    message: "{{error}}"
    then:
      function: schema
      functionOptions:
        schema:
          type: object
          additionalProperties: false
          required: [request, equals]
          properties:
            request:
              type: string
              minLength: 1
              description: Top-level request-body property whose value governs presence of this response property.
            equals:
              type: [boolean, string, number]
              description: Scalar literal the request field must equal for the property to be present (required, non-null).

  # Referential integrity for `x-present-when` (complements `present-when-shape`).
  # The shape rule cannot see that `request` names a real field, because the
  # marker sits on a shared component schema with no back-reference to the
  # operation whose request body it constrains. This rule resolves each
  # operation, finds every marker reachable from its 2xx responses, and asserts
  # `request` is a top-level property of that operation's request body (and that
  # the annotated property stays nullable + required — inert on the wire). A
  # dangling binding (`request: withLeese`) would otherwise silently degrade the
  # dependent typing every SDK generator derives from the marker.
  present-when-request-resolves:
    description: "Each `x-present-when.request` must name a top-level property of its operation's request body, and the annotated response property must stay nullable and required."
    severity: error
    given: "$.paths[*]"
    message: "{{error}}"
    then:
      function: verifyPresentWhenRequestResolves

  # ── x-required-permissions (camunda/camunda#54727) ──
  # Canonical endpoint -> required-permission(s) binding. Two rules:
  #   1. Schema-shape lint for each entry (built-in `schema` function).
  #   2. Completeness + registry-validity (custom verifyRequiredPermissions):
  #      every operation must declare it, and each static pair must be valid
  #      per resource-permissions.json.
  # See docs/adr/security/001-endpoint-required-permission-mapping.md.
  required-permissions-shape:
    description: "Each `x-required-permissions` entry must conform to the documented schema."
    severity: error
    given: $.paths[*][get,put,post,patch,delete].x-required-permissions[*]
    then:
      function: schema
      functionOptions:
        schema:
          type: object
          additionalProperties: false
          minProperties: 1
          properties:
            resourceType:
              type: string
              pattern: "^[A-Z][A-Z0-9_]+$"
              description: A member of AuthorizationResourceType. Pairs with permissionType.
            permissionType:
              type: string
              pattern: "^[A-Z][A-Z0-9_]+$"
              description: A member of PermissionType supported by resourceType.
            anyOf:
              type: array
              minItems: 2
              description: OR-group — at least one of the listed permissions suffices.
              items:
                type: object
                required: [resourceType, permissionType]
                additionalProperties: false
                properties:
                  resourceType: { type: string, pattern: "^[A-Z][A-Z0-9_]+$" }
                  permissionType: { type: string, pattern: "^[A-Z][A-Z0-9_]+$" }
            dynamic:
              type: boolean
              description: Set to `true` when the required permission is resolved at runtime from the request body/path.
            note:
              type: string
              minLength: 1
              description: Required when `dynamic` is true — explains how the permission is resolved.
          dependentRequired:
            dynamic: [note]
          oneOf:
            - required: [resourceType, permissionType]
            - required: [anyOf]
            - required: [dynamic, note]

  verify-required-permissions:
    description: "Every operation must declare `x-required-permissions` (or `[]` if unrestricted), and each static `{resourceType, permissionType}` pair must be valid per resource-permissions.json."
    severity: error
    given: "$"
    message: "{{error}}"
    then:
      function: verifyRequiredPermissions

  # ── x-permission-enforcement (camunda/camunda#54727) ──
  # Optional operation-level marker describing the *deny outcome* when the caller
  # lacks the required permission(s):
  #   - `reject` (default when absent): the caller receives a 4xx (403 for writes,
  #     404/not-found for single-resource point reads) and no data.
  #   - `filter`: the caller receives 200 with results scoped to the resources they
  #     are authorized for (collection/aggregate reads pushed through the search
  #     client as an authorization filter; also `activateJobs`).
  # Consumed by the spec-driven runtime auth test (api-test-generator#374) to assert
  # the correct deny shape. See docs/adr/security/001-endpoint-required-permission-mapping.md (D8).
  permission-enforcement-shape:
    description: "`x-permission-enforcement` must be either `reject` or `filter`."
    severity: error
    given: $.paths[*][get,put,post,patch,delete].x-permission-enforcement
    then:
      function: schema
      functionOptions:
        schema:
          type: string
          enum: [reject, filter]

  # ── x-semantic-establishes / x-semantic-requires (camunda/camunda#52272) ──
  # Three rules:
  #   1. Schema-shape lint for x-semantic-establishes (operation-level).
  #   2. Schema-shape lint for x-semantic-requires (operation-level).
  #   3. Cross-document check: every kind: must be in semantic-kinds.json,
  #      and every required kind must be established somewhere.
  # The first two use Spectral's built-in `schema` function (no custom code);
  # the third uses verifySemanticKindsRegistered.

  semantic-establishes-shape:
    description: "`x-semantic-establishes` must conform to the documented schema."
    severity: error
    given: $.paths[*][get,put,post,patch,delete].x-semantic-establishes
    then:
      function: schema
      functionOptions:
        schema:
          type: object
          required: [kind, identifiedBy]
          additionalProperties: false
          properties:
            kind:
              type: string
              pattern: "^[A-Z][A-Za-z0-9]+$"
              description: PascalCase singular noun. The symbolic name of the established thing, not of the operation.
            shape:
              type: string
              enum: [entity, edge]
              description: Optional discriminator. Defaults to `entity`. Use `edge` for membership-style relationships observable only via member-list search.
            identifiedBy:
              type: array
              minItems: 1
              description: Ordered tuple of identifying members. Length 1 for single-key entities, length ≥ 2 for composite-key entities and edges.
              items:
                type: object
                required: [in, name, semanticType]
                additionalProperties: false
                properties:
                  in: { type: string, enum: [body, path, query, header] }
                  name: { type: string, minLength: 1 }
                  semanticType:
                    type: string
                    pattern: "^[A-Z][A-Za-z0-9]+$"
                  acceptsExternal:
                    type: boolean
                    description: |
                      Per-tuple opt-out for the producer-existence cross-reference (camunda/camunda#52322).
                      Set to `true` when this specific edge endpoint accepts either an in-API producer
                      (the canonical local entity) OR an externally-minted ID (e.g. BYOG/OIDC IdP-supplied
                      group IDs at `assignGroupToRole` / `assignGroupToTenant`). The single-owner
                      identifier resolution still runs (typo / config check), but the resolved entity kind
                      is NOT pushed onto the producer-existence required-set for this tuple. Distinct from
                      kind-level `shape: external-entity` (which marks the kind as having no in-API
                      producer at all).

  semantic-requires-shape:
    description: "`x-semantic-requires` must conform to the documented schema."
    severity: error
    given: $.paths[*][get,put,post,patch,delete].x-semantic-requires
    then:
      function: schema
      functionOptions:
        schema:
          type: object
          required: [kind, bind]
          additionalProperties: false
          properties:
            kind:
              type: string
              pattern: "^[A-Z][A-Za-z0-9]+$"
            bind:
              type: object
              minProperties: 1
              description: Maps members of the established tuple to local parameters of the consumer.
              additionalProperties:
                type: object
                required: [from, name]
                additionalProperties: false
                properties:
                  from: { type: string, enum: [body, path, query, header] }
                  name: { type: string, minLength: 1 }

  verify-semantic-kinds-registered:
    description: "Every `kind:` referenced by x-semantic-establishes / x-semantic-requires must appear in semantic-kinds.json, every required kind must be established somewhere in the spec, and every identifiedBy/bind member must reference a parameter or top-level requestBody property that actually exists on the operation."
    severity: error
    given: "$"
    message: "{{error}}"
    then:
      function: verifySemanticKindsRegistered

  properties-added-in-version-shape:
    description: "`x-properties-added-in-version` must be a non-empty array of objects, each requiring `propertyName` plus at least one of `addedInVersion`/`addedInVersionOverride`, and no other keys."
    severity: error
    # Report at the schema's source location, not at every $ref consumer.
    resolved: false
    given: $..x-properties-added-in-version
    then:
      function: schema
      functionOptions:
        schema:
          type: array
          minItems: 1
          items:
            type: object
            required: [propertyName]
            additionalProperties: false
            anyOf:
              - required: [addedInVersion]
              - required: [addedInVersionOverride]
            properties:
              propertyName:
                type: string
                minLength: 1
                description: Name of the property (or filter operator like "$eq") this version annotation applies to.
              addedInVersion:
                type: string
                pattern: "^[0-9]+\\.[0-9]+(\\.[0-9]+)?$"
                description: Camunda version in which the property was introduced, e.g. "8.8".
              addedInVersionOverride:
                type: string
                pattern: "^[0-9]+\\.[0-9]+(\\.[0-9]+)?$"
                description: >-
                  A documented, reviewed exception: replaces the version the bundler computed
                  for this property before the usual Rule 1/2/3 evaluation, so `addedInVersion`
                  is only required alongside it when the overridden value still needs an
                  explicit annotation (i.e. it isn't inferred from the endpoint/ancestor
                  version). See "Overriding the computed version" in
                  .github/scripts/x-added-in-version-check/README.md.

  added-in-version-override-shape:
    description: "`x-added-in-version-override`, when present on an operation, must be a version string."
    severity: error
    given: $.paths[*][*].x-added-in-version-override
    then:
      function: schema
      functionOptions:
        schema:
          type: string
          pattern: "^[0-9]+\\.[0-9]+(\\.[0-9]+)?$"

  # Guard against new schemas introducing ambiguous bare identifier properties.
  # Existing schemas are grandfathered via the allowlist below — do not add new
  # entries without discussion. See https://github.com/camunda/camunda/issues/52510
  no-ambiguous-identifier-property:
    description: "Schema properties must not use bare `id` or `key` — use qualified names like `entityNameId`."
    severity: error
    given: "$.components.schemas[*]"
    message: "{{error}}"
    then:
      function: verifyNoAmbiguousIdentifierProperty
      functionOptions:
        allowlist:
          # Grandfathered schemas — these pre-date the naming convention and
          # cannot be renamed without breaking published SDK consumers.
          CreateGlobalTaskListenerRequest: [id]
          GlobalTaskListenerResult: [id]
          GlobalTaskListenerSearchQueryFilterRequest: [id]

  # Same ban for truly-inline schemas (request/response bodies, parameters).
  # Uses `resolved: false` so $ref pointers (which point at component schemas
  # already covered above) are NOT followed — only schemas literally defined
  # inline are checked. No allowlist: inline schemas are new code by definition.
  no-ambiguous-identifier-property-inline:
    description: "Inline schema properties must not use bare `id` or `key` — use qualified names like `entityNameId`."
    severity: error
    resolved: false
    given:
      - "$.paths[*][*].requestBody.content[*].schema"
      - "$.paths[*][*].responses[*].content[*].schema"
      - "$.paths[*][*].parameters[*].schema"
    message: "{{error}}"
    then:
      function: verifyNoAmbiguousIdentifierProperty

  # Every operation must declare its security modes (camunda/camunda#54268).
  # See verifySecurityDeclared.js for the accepted shapes — either
  # `security: []` for publicly unauthenticated endpoints, or a list of the
  # conditionally-enforced schemes (`bearerAuth`, `basicAuth`) with empty
  # scopes. The conditional-enforcement metadata on the schemes themselves is
  # set in rest-api.yaml (camunda/camunda#53708).
  require-security-declared:
    description: "Every operation must declare a `security:` block."
    severity: error
    given: $.paths[*][*]
    message: "{{error}}"
    then:
      function: verifySecurityDeclared

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/camunda-camunda-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.