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

# Receive a Stripe webhook

> Where Stripe delivers events for one provider connection. You do not call this — you paste the URL into the webhook settings of the Stripe account you connected, and the connection id in the path is what tells us whose secret to verify the signature with.

**It is outside `/v1` on purpose.** The version prefix is a promise about endpoints you call from your own code; this URL lives in a setting inside somebody else's dashboard, and a new API version must never mean going and editing it.

**The signature is the only credential.** There is no token, and the connection id authorizes nothing on its own — a request that cannot produce a signature over the body with this connection's webhook secret is refused, always. There is no mode in which an unverifiable body is accepted.

**Retries are safe, and 202 is the answer either way.** Every accepted event is recorded under the provider's own event id for this connection, so a redelivery — a retry after a timeout, or a whole window replayed after an outage — takes effect exactly once and still answers `202`. Nothing is applied inside this request: it is accepted, and processed immediately afterwards. That is also why a `202` does not mean the change is already visible on your other reads.

**Out-of-order delivery is handled.** Providers do not promise order, so an event is compared against the newest one already applied to the same object before it is acted on. An event that is older than what is known is accepted and deliberately not applied — which is what keeps a late `customer.subscription.updated` from resurrecting a subscription that was cancelled after it.



## OpenAPI

````yaml /api-reference/openapi.json post /webhooks/stripe/{connectionID}
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:
  /webhooks/stripe/{connectionID}:
    post:
      tags:
        - Provider webhooks
      summary: Receive a Stripe webhook
      description: >-
        Where Stripe delivers events for one provider connection. You do not
        call this — you paste the URL into the webhook settings of the Stripe
        account you connected, and the connection id in the path is what tells
        us whose secret to verify the signature with.


        **It is outside `/v1` on purpose.** The version prefix is a promise
        about endpoints you call from your own code; this URL lives in a setting
        inside somebody else's dashboard, and a new API version must never mean
        going and editing it.


        **The signature is the only credential.** There is no token, and the
        connection id authorizes nothing on its own — a request that cannot
        produce a signature over the body with this connection's webhook secret
        is refused, always. There is no mode in which an unverifiable body is
        accepted.


        **Retries are safe, and 202 is the answer either way.** Every accepted
        event is recorded under the provider's own event id for this connection,
        so a redelivery — a retry after a timeout, or a whole window replayed
        after an outage — takes effect exactly once and still answers `202`.
        Nothing is applied inside this request: it is accepted, and processed
        immediately afterwards. That is also why a `202` does not mean the
        change is already visible on your other reads.


        **Out-of-order delivery is handled.** Providers do not promise order, so
        an event is compared against the newest one already applied to the same
        object before it is acted on. An event that is older than what is known
        is accepted and deliberately not applied — which is what keeps a late
        `customer.subscription.updated` from resurrecting a subscription that
        was cancelled after it.
      operationId: receiveStripeWebhook
      parameters:
        - name: connectionID
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: >-
            The provider connection this endpoint belongs to. Each connection
            has its own webhook secret, so each gets its own URL.
      requestBody:
        required: true
        description: >-
          The provider's event, exactly as it was signed. It is read as bytes
          and never re-serialized — what is stored has to be what the signature
          covered.
        content:
          application/json:
            schema:
              type: object
              description: A Stripe event object. Its shape is Stripe's, not ours.
      responses:
        '202':
          description: >-
            Accepted. The event is recorded and will be processed; a repeat of
            an event already recorded answers this too.
        '400':
          description: The signature did not verify, or the body carries no event id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  code: invalid_signature
                  message: signature verification failed
        '404':
          description: >-
            No such connection, or it has no webhook secret yet. The two are
            answered the same way: an unsigned caller learns nothing about which
            connection ids exist.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  code: connection_not_found
                  message: no such provider connection
        '429':
          $ref: '#/components/responses/RateLimited'
        '501':
          description: >-
            This server cannot verify provider webhooks, because the capability
            that decrypts a stored webhook secret is unavailable on it.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  code: provider_webhooks_unavailable
                  message: provider webhooks are not available on this server
      security: []
components:
  schemas:
    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.

````