> ## Documentation Index
> Fetch the complete documentation index at: https://docs.userkit.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Fetch an environment's recently revoked sessions

> What makes revocation immediate instead of eventual.

A session JWT is verified **offline** — that is the whole point of JWKS — so nothing can recall one. Ending a session stops the next refresh, and the token already in somebody's hand keeps verifying until its own `exp`. This document closes that gap: a verifier that holds a current copy refuses a revoked `sid` within the copy's cache window rather than within the token's lifetime.

It is the same kind of thing as JWKS and is used the same way — public, unauthenticated, addressed by the publishable key your config already holds, and **polled on a schedule rather than called per request**. Checking a token stays a set lookup against a document already in memory.

The response carries `Cache-Control: public, max-age=15, stale-while-revalidate=900, stale-if-error=900` — the stale window covers a refetch that is slow and one that fails alike, because a document you cannot refresh is more useful than no document. Both numbers matter and both are restated in the body:

- `max_age_seconds` is the bound you get. A revoked session stops verifying within it.
- `window_seconds` is how far back the document reaches. A copy older than that says nothing about the tokens alive now — discard it.

**When you cannot fetch it, keep verifying.** A revocation list that refuses every request whenever it is unreachable is a worse failure than the one it prevents: the correct degrade is back to the bound that always existed, the token's own few minutes. `@userkit/nextjs`'s `verifyContactToken` implements exactly this and reports which of the three cases it was in.

No CORS on this route, deliberately: the caller is a server. A revocation decision made in a page is a revocation decision an attacker controls.

Rate limited to 600 requests per minute per IP — the loosest limit on this surface, because polling it is the intended use.



## OpenAPI

````yaml /api-reference/openapi.json get /v1/revocations/{publishableKey}
openapi: 3.1.0
info:
  title: UserKit API
  version: 1.0.0
  description: >-
    The HTTP surface of UserKit.


    Two planes share one API. The **staff plane** is what a human uses in the
    panel: users, organizations, roles, members, keys. The **customer plane** is
    what a developer's own product uses: contacts, identities, hosted or
    federated sign-in.


    Every error answers the same envelope — `{"error": {"code", "message"}}`.
    The `code` is a stable contract to branch on; the `message` is for a person
    and may change.
servers:
  - url: '{baseUrl}'
    description: The API host.
    variables:
      baseUrl:
        default: https://api.userkit.dev
        description: Base URL of the API, no trailing slash.
security:
  - sessionToken: []
tags:
  - name: Authentication
    description: >-
      Public sign-up, sign-in, two-factor and password recovery for staff
      accounts.
  - name: Session
    description: >-
      The active session: who the caller is, which organization they are in, and
      signing out.
  - name: Account
    description: The caller's own account — profile, password, sessions, avatar.
  - name: Two-factor
    description: >-
      TOTP setup, activation and recovery codes. Returns 501 when two-factor is
      unavailable on the server.
  - name: Organizations
    description: The organizations a user belongs to, and the active one.
  - name: Environments
    description: >-
      The live and test environments seeded with every organization, and their
      identity settings.
  - name: Members
    description: Memberships and invitations.
  - name: Roles
    description: Roles and the permission catalogue they draw from.
  - name: Audit log
    description: >-
      What staff did inside an organization. Append-only, and read behind its
      own permission.
  - name: API keys
    description: Secret keys (`uk_sk_…`) and publishable keys (`uk_pk_…`).
  - name: Contacts (staff session)
    description: >-
      The staff view of the customer plane, opened by a staff session. Reads
      take `?environment=` as an explicit view parameter.
  - name: Contacts (API key)
    description: >-
      The machine surface, authenticated by an API key. The environment is the
      key's environment and cannot be named by the caller.
  - name: Customer plane
    description: >-
      Called from the developer's own pages with a publishable key: boot, magic
      link, and the contact's own session.
  - name: Hosted auth
    description: >-
      Sign-up, sign-in, verification and recovery for contacts, when UserKit
      owns the account.
  - name: Customer teams
    description: >-
      A customer is a team. Its roster, its invitations and its two roles —
      owner and member — administered by the contact's own session. The active
      customer travels in `X-Customer-Id`.
  - name: Webhooks
    description: >-
      Outbound webhooks: endpoints, the published event catalogue, the delivery
      log, replay and test sends. Never gated by a plan — webhooks are a
      developer primitive.
  - name: Catalogue
    description: >-
      The plans, prices and features **you** sell to your own customers. Per
      environment, a price per currency, and `recurring` or `one_time`. Distinct
      from the plan you are on with UserKit, which is `GET
      /v1/organization/entitlements`.
  - name: Subscriptions
    description: >-
      What one of **your** customers is paying you, mirrored from the gateway
      that charges them. The gateway is the truth about money — it holds the
      schedule, runs the retries and decides what a proration is worth — so
      these routes ask it to change something and answer with what it then said.
      `provider_synced_at` is how stale the copy admits to being.
  - name: Provider webhooks
    description: >-
      Where a payment provider delivers to. Not a surface you call — it is a URL
      you paste into the provider's dashboard, which is why it sits outside
      `/v1`: a version bump must never mean editing a setting in somebody else's
      product. Signed with the secret of the connection named in the path, and
      safe to retry.
  - name: Entitlements
    description: >-
      What one of **your** customers may do, resolved: the plan their
      subscription carries, plus the overrides you promised them on top. This is
      the read your own gate calls, so it is cached and answers in one round
      trip. It is a different question from `GET /v1/organization/entitlements`,
      which is the plan **you** are on with UserKit — two catalogues, same word,
      different money.
paths:
  /v1/revocations/{publishableKey}:
    get:
      tags:
        - Customer plane
      summary: Fetch an environment's recently revoked sessions
      description: >-
        What makes revocation immediate instead of eventual.


        A session JWT is verified **offline** — that is the whole point of JWKS
        — so nothing can recall one. Ending a session stops the next refresh,
        and the token already in somebody's hand keeps verifying until its own
        `exp`. This document closes that gap: a verifier that holds a current
        copy refuses a revoked `sid` within the copy's cache window rather than
        within the token's lifetime.


        It is the same kind of thing as JWKS and is used the same way — public,
        unauthenticated, addressed by the publishable key your config already
        holds, and **polled on a schedule rather than called per request**.
        Checking a token stays a set lookup against a document already in
        memory.


        The response carries `Cache-Control: public, max-age=15,
        stale-while-revalidate=900, stale-if-error=900` — the stale window
        covers a refetch that is slow and one that fails alike, because a
        document you cannot refresh is more useful than no document. Both
        numbers matter and both are restated in the body:


        - `max_age_seconds` is the bound you get. A revoked session stops
        verifying within it.

        - `window_seconds` is how far back the document reaches. A copy older
        than that says nothing about the tokens alive now — discard it.


        **When you cannot fetch it, keep verifying.** A revocation list that
        refuses every request whenever it is unreachable is a worse failure than
        the one it prevents: the correct degrade is back to the bound that
        always existed, the token's own few minutes. `@userkit/nextjs`'s
        `verifyContactToken` implements exactly this and reports which of the
        three cases it was in.


        No CORS on this route, deliberately: the caller is a server. A
        revocation decision made in a page is a revocation decision an attacker
        controls.


        Rate limited to 600 requests per minute per IP — the loosest limit on
        this surface, because polling it is the intended use.
      operationId: sessionRevocations
      parameters:
        - name: publishableKey
          in: path
          required: true
          schema:
            type: string
          description: >-
            `uk_pk_live_…` or `uk_pk_test_…`. The list is per environment, like
            the key that signed the token.
      responses:
        '200':
          description: >-
            The environment's recent revocations. An empty list is the normal
            answer and is not an error.
          headers:
            Cache-Control:
              schema:
                type: string
              description: >-
                `public, max-age=15, stale-while-revalidate=900,
                stale-if-error=900`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionRevocations'
              example:
                revoked:
                  - 6f1c2b4e-9d3a-4f28-8b21-0c7e5a9d1f34
                window_seconds: 900
                max_age_seconds: 15
                generated_at: '2026-07-29T12:00:00Z'
        '404':
          description: '`not_found` — unknown or malformed publishable key.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
      security: []
components:
  schemas:
    SessionRevocations:
      type: object
      description: >-
        The sessions revoked in this environment recently enough that a session
        JWT naming one could still be alive. Kept deliberately small: an older
        revocation names no token anybody could still present, so it is dropped
        rather than accumulated — this is a short list, never a growing
        certificate revocation list.
      properties:
        revoked:
          type: array
          description: Session ids. Match against a token's `sid` claim.
          items:
            type: string
            format: uuid
        window_seconds:
          type: integer
          description: >-
            How far back this document reaches. A copy older than this says
            nothing about the tokens alive now — discard it rather than trust
            it.
        max_age_seconds:
          type: integer
          description: >-
            How long a copy stays current. This is the bound the endpoint buys
            you: a revoked session stops verifying within this many seconds
            instead of within the token's full lifetime.
        generated_at:
          type: string
          format: date-time
    Error:
      type: object
      description: Every error in this API answers this envelope.
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              description: Stable. Branch on this.
            message:
              type: string
              description: For a person. May change.
          required:
            - code
            - message
      required:
        - error
      example:
        error:
          code: forbidden
          message: your role does not allow this action
  responses:
    RateLimited:
      description: >-
        `rate_limited` — too many requests. `Retry-After` carries the window in
        seconds.


        The counters are shared across instances. When that store cannot be
        reached each instance counts on its own instead: the limits get looser,
        never absent.
      headers:
        Retry-After:
          schema:
            type: integer
          description: Seconds until the window resets.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  securitySchemes:
    sessionToken:
      type: http
      scheme: bearer
      description: >-
        A staff session token, `uk_st_…`. Minted by sign-up, sign-in or the
        two-factor exchange.

````