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

# List payment providers

> Requires `billing:read`. The payment gateways this environment can charge through — the gateway **you** sell your product through, not what UserKit charges you.

One entry per provider the product speaks of, connected or not: the screen is a list of what is possible. `connected` says a row exists; `status` says whether it works, and the two are different questions — a connection that exists and is unhealthy is not the same as no connection.

**Every status that is not `connected` names a different repair.** That is what it is for: collapsing them into one "broken" ends with a screen telling you to re-paste a credential that is perfect.

| `status` | what it means | the repair |
|---|---|---|
| `pending` | the keys are stored and the provider has not been asked | run the health check |
| `connected` | the last check succeeded | none |
| `invalid_credentials` | the gateway refused the keys | rotate them |
| `mode_mismatch` | live-mode keys on a test environment, or the reverse | paste the **other** key, not a new one |
| `account_not_ready` | the keys work and the account cannot take money: a sign-up never finished, payouts restricted, an account closed | in the gateway's own dashboard — nothing about the keys will fix it |

`status_detail` carries the gateway's own sentence, for a person to read. The status says which repair; the detail says why. Never parse it.

Neither secret is ever returned. The secret key is write-only by design, and `webhook_secret_set` is the one thing this answers about the webhook secret: whether an endpoint has been registered at the provider.

`capabilities` reports what the gateway can do **beyond the minimum every gateway can do**, so a screen stops offering an act that would be refused. It is absent when the server has no adapter loaded, because an empty object would read as a claim about the provider rather than about the deployment.



## OpenAPI

````yaml /api-reference/openapi.json get /v1/organization/environments/{id}/billing-providers
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:
    get:
      tags:
        - Environments
      summary: List payment providers
      description: >-
        Requires `billing:read`. The payment gateways this environment can
        charge through — the gateway **you** sell your product through, not what
        UserKit charges you.


        One entry per provider the product speaks of, connected or not: the
        screen is a list of what is possible. `connected` says a row exists;
        `status` says whether it works, and the two are different questions — a
        connection that exists and is unhealthy is not the same as no
        connection.


        **Every status that is not `connected` names a different repair.** That
        is what it is for: collapsing them into one "broken" ends with a screen
        telling you to re-paste a credential that is perfect.


        | `status` | what it means | the repair |

        |---|---|---|

        | `pending` | the keys are stored and the provider has not been asked |
        run the health check |

        | `connected` | the last check succeeded | none |

        | `invalid_credentials` | the gateway refused the keys | rotate them |

        | `mode_mismatch` | live-mode keys on a test environment, or the reverse
        | paste the **other** key, not a new one |

        | `account_not_ready` | the keys work and the account cannot take money:
        a sign-up never finished, payouts restricted, an account closed | in the
        gateway's own dashboard — nothing about the keys will fix it |


        `status_detail` carries the gateway's own sentence, for a person to
        read. The status says which repair; the detail says why. Never parse it.


        Neither secret is ever returned. The secret key is write-only by design,
        and `webhook_secret_set` is the one thing this answers about the webhook
        secret: whether an endpoint has been registered at the provider.


        `capabilities` reports what the gateway can do **beyond the minimum
        every gateway can do**, so a screen stops offering an act that would be
        refused. It is absent when the server has no adapter loaded, because an
        empty object would read as a claim about the provider rather than about
        the deployment.
      operationId: listBillingProviders
      parameters:
        - $ref: '#/components/parameters/OrganizationHeader'
        - $ref: '#/components/parameters/EnvironmentId'
      responses:
        '200':
          description: The connections.
          content:
            application/json:
              schema:
                type: object
                properties:
                  environment:
                    type: string
                    enum:
                      - live
                      - test
                  providers:
                    type: array
                    items:
                      $ref: '#/components/schemas/BillingProviderConnection'
                  billing_providers_available:
                    type: boolean
                    description: >-
                      Whether the server can hold a gateway credential at all.
                      False means connecting would answer `501`, whatever is
                      listed above.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
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.
  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
    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.
    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'
  securitySchemes:
    sessionToken:
      type: http
      scheme: bearer
      description: >-
        A staff session token, `uk_st_…`. Minted by sign-up, sign-in or the
        two-factor exchange.

````