> ## 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.

# What one of your customers is entitled to

> Requires `billing:read`. The resolved answer for one of **your** customers: the plan their subscription carries, with the overrides you promised them applied on top and the expired ones already dropped.

This is the support view of the same read your product's gate makes (`GET /v1/customers/{id}/entitlements` with a server key, or `GET /v1/contact/entitlements` from the customer's own session) — the conversation that starts with "they say they paid for this".

Which subscription answers, when a customer has more than one: the one that grants the most access, and the newest of those. `past_due` still grants — a failed payment is a payment your gateway is still retrying, and the gateway's dunning settings are what decide when retrying becomes cancelling. `incomplete`, `paused` and `canceled` grant nothing and come back as `plan: none`.



## OpenAPI

````yaml /api-reference/openapi.json get /v1/organization/customers/{id}/entitlements
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/organization/customers/{id}/entitlements:
    get:
      tags:
        - Entitlements
      summary: What one of your customers is entitled to
      description: >-
        Requires `billing:read`. The resolved answer for one of **your**
        customers: the plan their subscription carries, with the overrides you
        promised them applied on top and the expired ones already dropped.


        This is the support view of the same read your product's gate makes
        (`GET /v1/customers/{id}/entitlements` with a server key, or `GET
        /v1/contact/entitlements` from the customer's own session) — the
        conversation that starts with "they say they paid for this".


        Which subscription answers, when a customer has more than one: the one
        that grants the most access, and the newest of those. `past_due` still
        grants — a failed payment is a payment your gateway is still retrying,
        and the gateway's dunning settings are what decide when retrying becomes
        cancelling. `incomplete`, `paused` and `canceled` grant nothing and come
        back as `plan: none`.
      operationId: customerEntitlements
      parameters:
        - $ref: '#/components/parameters/OrganizationHeader'
        - $ref: '#/components/parameters/EnvironmentQuery'
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: >-
            The customer, resolved inside the environment above. An id from the
            other environment answers 404.
      responses:
        '200':
          description: The resolved grant.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EntitlementGrant'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '503':
          $ref: '#/components/responses/EntitlementsUnavailable'
components:
  parameters:
    OrganizationHeader:
      name: X-Organization-Id
      in: header
      required: false
      schema:
        type: string
      description: >-
        The organization the caller is acting on — the `org_…` code that appears
        in the panel URL. It *identifies*; the membership JOIN is what
        *authorizes*, so a forged code reads nothing. Absent, the session's
        default organization answers.
    EnvironmentQuery:
      name: environment
      in: query
      required: false
      schema:
        type: string
        enum:
          - live
          - test
        default: live
      description: >-
        Which environment to act in. A view parameter, valid only on the staff
        surface — a machine credential never chooses its environment, it is
        resolved from the key.
  schemas:
    EntitlementGrant:
      type: object
      description: >-
        What one of your customers may do, resolved: their subscription's plan,
        with the overrides you promised them applied on top. This is the answer
        your own gate should read — the merge is done here so you never
        reimplement it from the pieces.


        Every feature in the environment's catalogue is present, including the
        ones the plan does not carry, so a client can render an offer rather
        than an absence. A feature the plan carries and an override withholds is
        present and `"enabled": false`.
      properties:
        customer_id:
          type: string
          format: uuid
        plan:
          type: string
          description: >-
            The `key` of the plan behind this answer. `none` means the customer
            is on no plan — never subscribed, or the only subscription is
            cancelled — which is a complete answer and not an error. `unknown`
            never appears in a `200`: an answer nobody could resolve is a `503`,
            not a plan.
          example: pro
        resolved:
          type: boolean
          description: >-
            Always `true` in a `200` here. The field exists because it is
            `false` on `GET /v1/organization/entitlements`, where an unresolved
            read still has a truthful body to send — see that operation.
        features:
          type: object
          description: Keyed by **your** feature key, the string your own code gates on.
          additionalProperties:
            type: object
            properties:
              kind:
                type: string
                enum:
                  - boolean
                  - metered
                  - credit
                description: Which of the two numbers below means anything.
              enabled:
                type: boolean
                description: >-
                  For a `metered` feature this is derived from the ceiling: a
                  limit of `0` is the absence of the feature, and any allowance
                  — including unlimited — is its presence. For a `credit`
                  feature it is true whenever the plan carries it, even when it
                  grants none: a balance of zero is still a balance you can top
                  up.
              limit:
                type:
                  - integer
                  - 'null'
                format: int64
                description: >-
                  The ceiling on a `metered` feature. `null` means there is no
                  NUMBER here, which is not one answer — **read `unlimited`
                  before you compare anything against it.** `null` with
                  `"unlimited": true` is an allowance with no ceiling; `null`
                  with `"unlimited": false` is a ceiling of zero: a feature this
                  customer is not entitled to, or a kind that is not measured
                  against a ceiling. The unlimited sentinel never reaches the
                  wire, because a magic number is one every client has to be
                  told about — the boolean beside it is how it travels instead.
              unlimited:
                type: boolean
                description: >-
                  Whether the allowance really has no ceiling, said in a field
                  rather than left to be inferred from `limit: null`. A gate
                  that treats the bare null as unlimited grants unmetered use of
                  a feature the plan does not carry, which is the reading this
                  field exists to remove. `false` on every kind that is not a
                  ceiling.
              included:
                type:
                  - integer
                  - 'null'
                format: int64
                description: >-
                  How many units a `credit` feature grants per period — `0` when
                  the plan carries the feature and grants none of it, which is a
                  balance you can top up. `null` on the other kinds, and `null`
                  on a `credit` feature the customer is not entitled to at all:
                  `enabled` is what tells those two apart. Never a sentinel — a
                  plan cannot grant an unlimited balance. Its own field rather
                  than sharing `limit`, because a ceiling is compared against
                  usage and a grant is added to a balance — one field would
                  leave you guessing which you were holding.
    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:
    Unauthorized:
      description: >-
        `unauthorized` — missing, malformed or expired credential. A well-formed
        credential from the wrong family says so: "this endpoint expects a staff
        session token, not an organization API key".
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: '`forbidden` — your role does not allow this action.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: >-
        `not_found`. Also the answer for a resource that exists in another
        organization or environment — the 404 never reveals which.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    EntitlementsUnavailable:
      description: >-
        `entitlements_unavailable` — the answer could not be resolved, and
        **nothing is being refused**.


        This is a refusal to assert rather than a refusal to allow, and the
        distinction is the whole reason it is not a `200` with everything
        switched off. An entitlement read fails **open** by design: an outage
        here must not become an outage in your product. But the catalogue that
        would say which features exist is exactly what could not be read, so
        there is no truthful body to send — and a body full of `"enabled":
        false` would make the decision for you, in the direction the rule
        forbids.


        So the direction stays yours. Keep your last known answer, open the
        gate, or close it: it is your product and your money. `Retry-After` is a
        second, because what is being waited on is a read coming back rather
        than a queue draining.
      headers:
        Retry-After:
          schema:
            type: integer
          description: Seconds to wait before asking again.
      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.

````