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

# Check a payment provider connection

> Requires `billing:connect`, for a route that reads: it decrypts the key that moves money and sends it to a third party, which is an act of the same weight as storing it.

This is the only thing that writes `status`, and it distinguishes two failures that must never fuse. **The gateway answering "no"** is news about the connection and becomes `invalid_credentials`, `mode_mismatch` or `account_not_ready` — three answers, because each names a different repair. **Not having been able to ask** is weather: it answers `503`, records the attempt, and leaves `status` exactly as it was — a status that recorded a blip would tell a screen a connection broke on a day nothing about it changed, and the next move would be a developer rotating keys that were fine.

A successful check answering `invalid_credentials` is still a `200`: the check ran, and its result is the body.



## OpenAPI

````yaml /api-reference/openapi.json post /v1/organization/environments/{id}/billing-providers/{provider}/health
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/environments/{id}/billing-providers/{provider}/health:
    post:
      tags:
        - Environments
      summary: Check a payment provider connection
      description: >-
        Requires `billing:connect`, for a route that reads: it decrypts the key
        that moves money and sends it to a third party, which is an act of the
        same weight as storing it.


        This is the only thing that writes `status`, and it distinguishes two
        failures that must never fuse. **The gateway answering "no"** is news
        about the connection and becomes `invalid_credentials`, `mode_mismatch`
        or `account_not_ready` — three answers, because each names a different
        repair. **Not having been able to ask** is weather: it answers `503`,
        records the attempt, and leaves `status` exactly as it was — a status
        that recorded a blip would tell a screen a connection broke on a day
        nothing about it changed, and the next move would be a developer
        rotating keys that were fine.


        A successful check answering `invalid_credentials` is still a `200`: the
        check ran, and its result is the body.
      operationId: checkBillingProvider
      parameters:
        - $ref: '#/components/parameters/OrganizationHeader'
        - $ref: '#/components/parameters/EnvironmentId'
        - $ref: '#/components/parameters/BillingProvider'
      responses:
        '200':
          description: The provider answered. `status` is what it said.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/BillingProviderConnection'
                  - type: object
                    properties:
                      account_name:
                        type: string
                        description: >-
                          What the gateway just said the account is called, so a
                          screen can confirm you reached the RIGHT account and
                          not merely one that works. Only this route has it: it
                          is not stored, because a name we cached would go stale
                          where nobody would look.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: >-
            The environment does not exist, `unknown_provider`, or `not_found` —
            nothing is connected.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '501':
          description: >-
            `billing_provider_unavailable` — reading a stored credential is
            unavailable on the server. `provider_unavailable` — the server has
            no adapter for that gateway. `health_check_unsupported` — the
            gateway cannot be asked whether a credential still works, which is
            not the same as a gateway that cannot charge.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: >-
            `provider_unreachable` — the provider could not be reached. The
            connection's stored state is unchanged; retry.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
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.
    EnvironmentId:
      name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: The environment's id.
    BillingProvider:
      name: provider
      in: path
      required: true
      description: >-
        The payment gateway. `stripe` today. A gateway UserKit has no adapter
        for is refused here rather than stored, because a connection nothing can
        charge is discovered at somebody's checkout.
      schema:
        type: string
        enum:
          - stripe
  schemas:
    BillingProviderConnection:
      type: object
      description: >-
        One payment gateway, as this environment holds it. Both secrets are
        absent by design: the secret key is write-only, and `webhook_secret_set`
        is the only thing said about the webhook secret.


        `connected` and `status` answer different questions — a connection that
        exists and is unhealthy is not the same as no connection, and neither is
        a provider this server has no adapter for (`adapter_available`).
      properties:
        provider:
          type: string
          example: stripe
        connected:
          type: boolean
          description: >-
            Whether credentials have been stored for this provider in this
            environment. The fields below it are present only when true.
        adapter_available:
          type: boolean
          description: >-
            Whether this server can talk to the gateway at all. False means the
            health check answers `501` whatever else is reported here.
        capabilities:
          $ref: '#/components/schemas/BillingProviderCapabilities'
        id:
          type: string
          format: uuid
          description: The connection's id. It is what a webhook endpoint is addressed by.
        status:
          type: string
          enum:
            - pending
            - connected
            - invalid_credentials
            - mode_mismatch
            - account_not_ready
          description: >-
            Each value that is not `connected` names a different repair — see
            the endpoint description.
        status_detail:
          type: string
          description: >-
            The gateway's own sentence, for a person to read. The status says
            which repair; this says why. Never parse it.
        provider_account_id:
          type: string
          description: >-
            The merchant account the credentials open, as the gateway names it.
            Empty until a health check has asked, and never erased by a later
            refusal — the catalogue's price mappings join by this value.
        publishable_key:
          type: string
          description: The gateway's browser-side key, where it has one. Public by design.
        webhook_secret_set:
          type: boolean
          description: Whether a webhook endpoint has been registered at the gateway.
        last_verified_at:
          type: string
          format: date-time
          nullable: true
          description: >-
            Null means the gateway has never been asked. It moves even when the
            check could not be completed, which is what separates a fresh
            `pending` from a stale `connected`.
        updated_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
    BillingProviderCapabilities:
      type: object
      description: >-
        What a gateway can do **beyond the minimum every gateway can do**. Every
        flag defaults to false, so a provider that declares nothing is treated
        as the weakest one — silence is never a claim. Read it to stop offering
        an act that would be refused.


        **Every flag below is always present.** The object is emitted whole or
        not at all: absent means this server has no adapter loaded for the
        gateway, and it never means "some flags were left out". A reader must
        therefore distinguish `false` from missing rather than testing for
        falsiness — a key that is not here is not a gateway that cannot do
        something, it is a question nobody asked.
      required:
        - scheduled_plan_change
        - proration
        - cancel_at_period_end
        - hosted_portal
        - tax_calculation
        - resume_subscription
        - list_invoices
        - list_catalogue
      properties:
        scheduled_plan_change:
          type: boolean
          description: >-
            A plan change that takes effect at the end of the current period
            rather than now. Without it, asking for one is refused rather than
            applied immediately — which would be a charge the customer did not
            agree to.
        proration:
          type: boolean
          description: >-
            Money already paid can be credited or charged pro rata on a
            mid-period change. Without it, the new price applies from the next
            period.
        cancel_at_period_end:
          type: boolean
          description: >-
            A cancellation that is a schedule rather than an act. Without it,
            cancelling is immediate.
        hosted_portal:
          type: boolean
          description: >-
            The gateway hosts a page where the customer manages their own
            subscription.
        tax_calculation:
          type: boolean
          description: >-
            The gateway computes tax. UserKit never calculates it — the
            catalogue stores whether a price includes tax and nothing more.
        resume_subscription:
          type: boolean
          description: A scheduled cancellation can be undone.
        list_invoices:
          type: boolean
          description: The gateway has an invoice history to read.
        list_catalogue:
          type: boolean
          description: >-
            The gateway's own catalogue can be read, so an existing one can be
            imported instead of retyped. False where the gateway has a price and
            no catalogue above it.
  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'
  securitySchemes:
    sessionToken:
      type: http
      scheme: bearer
      description: >-
        A staff session token, `uk_st_…`. Minted by sign-up, sign-in or the
        two-factor exchange.

````