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

# Read link status

> Returns the current link for one of your accounts, including links
that are no longer active (`UNLINKED`, `LINK_REVOKED`).

**Treat `404` as "not linked."** It is the normal answer for an account
that has never been connected. A link becomes addressable by
`partnerUserId` only once your `oauth-link` call completes it, so a
`404` never means "start over" on its own — if you have an
`oauth-link` call in flight, wait for its response.

Bilt does not push link-state changes to partners; poll this endpoint
or check it at the point of use if you need to learn that a member
unlinked from the Bilt side.




## OpenAPI

````yaml /account-linking/account-links-openapi.yaml get /v1/account-links/{partnerUserId}
openapi: 3.0.3
info:
  title: Bilt Account Linking SDK
  version: 1.0.0
  description: >
    Endpoint reference for the `/v1/account-links` API — the partner-facing
    surface any external

    partner calls to link and unlink its customers' accounts with Bilt members.
    The integration

    contract (environments, authentication, the `id_token` flow, idempotency,
    link states and the

    normative error table) is the

    [Partner to Bilt Account Linking](/account-linking/account-links-api) guide;
    these pages are

    its §7 as a browsable reference and never say more than it does.
servers:
  - url: https://partnerapi.biltrewards.com
    description: Production — partner (third-party) gateway
  - url: https://staging.partnerapi.biltrewards.com
    description: Staging — partner (third-party) gateway
security:
  - partnerBearer: []
  - apiKey: []
tags:
  - name: Linking
    description: >-
      Linking that starts in your app — OIDC against Bilt's IdP, then one call
      here.
  - name: Link status
    description: Read the current link for one of your accounts.
  - name: Unlinking
    description: Break a link from your side.
  - name: CIBA linking
    description: >-
      Settle a link that started in the Bilt app — only for integrations whose
      agreement includes CIBA.
paths:
  /v1/account-links/{partnerUserId}:
    get:
      tags:
        - Link status
      summary: Read link status
      description: |
        Returns the current link for one of your accounts, including links
        that are no longer active (`UNLINKED`, `LINK_REVOKED`).

        **Treat `404` as "not linked."** It is the normal answer for an account
        that has never been connected. A link becomes addressable by
        `partnerUserId` only once your `oauth-link` call completes it, so a
        `404` never means "start over" on its own — if you have an
        `oauth-link` call in flight, wait for its response.

        Bilt does not push link-state changes to partners; poll this endpoint
        or check it at the point of use if you need to learn that a member
        unlinked from the Bilt side.
      operationId: partnerGetLinkStatus
      parameters:
        - $ref: '#/components/parameters/PartnerUserId'
        - $ref: '#/components/parameters/XRequestId'
      responses:
        '200':
          description: The link keyed to this account, in whatever state it is in.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LinkResponse'
              examples:
                linked:
                  summary: Active link
                  value:
                    linkId: 9f1c2e4a-7b30-4d55-8e21-6c0b9a3f5d78
                    partnerUserId: acme_customer_sandbox_123
                    linkStatus: LINKED
                    linkedAt: '2026-07-31T13:45:00Z'
                    partnerDetails: {}
                revoked:
                  summary: Broken from your side via POST /unlink
                  value:
                    linkId: 9f1c2e4a-7b30-4d55-8e21-6c0b9a3f5d78
                    partnerUserId: acme_customer_sandbox_123
                    linkStatus: LINK_REVOKED
                    linkedAt: '2026-07-31T13:45:00Z'
                    partnerDetails: {}
        '400':
          $ref: '#/components/responses/ValidationFailure'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/PartnerAccountNotFound'
        '500':
          $ref: '#/components/responses/ServerError'
        '502':
          $ref: '#/components/responses/IdpUnavailable'
components:
  parameters:
    PartnerUserId:
      name: partnerUserId
      in: path
      required: true
      description: |
        Your opaque, stable identifier for the account — the value you sent
        on `oauth-link`. Never an email address or phone number.
      schema:
        type: string
        minLength: 1
        maxLength: 256
        example: acme_customer_sandbox_123
    XRequestId:
      name: x-request-id
      in: header
      required: false
      description: |
        Optional but strongly recommended: a unique id per request (a UUID is
        fine). Echoed back as `error.requestId` so your logs and Bilt's line
        up. Quote it in every support ticket.
      schema:
        type: string
        example: 6f6b1afd-22e0-4137-a096-b15e41cdc890
  schemas:
    LinkResponse:
      type: object
      description: |
        Partner-facing view of a link. Returned by `oauth-link` (without
        `partnerDetails`) and by the status read.
      required:
        - linkId
        - partnerUserId
        - linkStatus
      properties:
        linkId:
          type: string
          format: uuid
          description: |
            The id of this link **episode**, and the value `POST /unlink`
            requires. One account legitimately owns several historical links,
            so ending one means naming which. Published on both the create
            response and the status read, so you can always recover it rather
            than having to store it.
          example: 9f1c2e4a-7b30-4d55-8e21-6c0b9a3f5d78
        partnerUserId:
          type: string
          description: Your opaque user id, echoed back.
          example: acme_customer_sandbox_123
        linkStatus:
          $ref: '#/components/schemas/LinkStatus'
        linkedAt:
          type: string
          format: date-time
          nullable: true
          description: When the link became `LINKED`, ISO-8601 UTC.
          example: '2026-07-31T13:45:00Z'
        partnerDetails:
          type: object
          additionalProperties: true
          nullable: true
          description: |
            Non-sensitive metadata Bilt holds about the link. Present on the
            status read only; empty (`{}`) for this integration model —
            reserved for future use.
    LinkStatus:
      type: string
      description: |
        The states you can observe on the `oauth-link` response and the
        status read. `LINKED` is the only usable state. `UNLINKED` and
        `LINK_REVOKED` are terminal for that link; the next successful
        `oauth-link` creates a fresh one. `UNLINKING_IN_PROGRESS` is only
        reachable for integrations where Bilt calls you on unlink — unusable
        but not terminal; re-read later. Do not code against values outside
        this list.
      enum:
        - LINKED
        - UNLINKED
        - LINK_REVOKED
        - UNLINKING_IN_PROGRESS
      example: LINKED
    ConstraintViolationResponse:
      type: object
      description: |
        The shape of every validation `400` on this API — **not** the error
        envelope. Documented so you do not write a parser that assumes
        `error.code` on every non-2xx response.
      required:
        - title
        - status
        - violations
      properties:
        title:
          type: string
          example: Constraint Violation
        status:
          type: integer
          example: 400
        violations:
          type: array
          items:
            type: object
            properties:
              field:
                type: string
                example: partnerUserId
              message:
                type: string
                example: must not be blank
    ErrorResponse:
      type: object
      description: |
        Error envelope for every business failure (`401`, `404`, `409`,
        `500`, `502`, and the CIBA `400 INVALID_STATE_TOKEN`). Bean Validation
        failures do **not** use it — see `ConstraintViolationResponse`. Parse
        leniently: the envelope may carry additional fields beyond the three
        documented here, and none of them changes how you should react.
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
          properties:
            code:
              type: string
              description: >-
                **Branch on this, never on `message`.** Codes are stable;
                messages are not.
              example: ID_TOKEN_REPLAYED
            message:
              type: string
              description: Detail for your logs. Not intended for end users.
              example: id_token has already been used
            requestId:
              type: string
              description: |
                Your `x-request-id` when you sent one, else a generated id.
                Empty whenever your credential is refused — the `401` for a
                missing, unrecognized or disabled API key or partner access
                token, and the `502 IDP_UNAVAILABLE` raised while validating a
                partner access token — because those are rejected before the
                correlation step runs (guide §9.1).
              example: 6f6b1afd-22e0-4137-a096-b15e41cdc890
  responses:
    ValidationFailure:
      description: |
        A field was missing, blank, or too long. **Not the error envelope** —
        the framework answers with a `ConstraintViolationResponse`. **Treat
        the body as optional:** a body that is not valid JSON, or a value of
        the wrong type (a non-UUID `linkId`), fails deserialization first and
        answers `400` with no parseable body at all, so branch on the status
        before you parse. Fix the request; do not retry as-is.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ConstraintViolationResponse'
          example:
            title: Constraint Violation
            status: 400
            violations:
              - field: partnerUserId
                message: must not be blank
    Unauthorized:
      description: |
        Credential missing, not recognized, or disabled — or a bearer token
        Bilt rejected (there is no fallback to the API key). This `401` is
        raised before the correlation step and carries an empty `requestId`;
        quote the timestamp and endpoint instead.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: UNAUTHORIZED
              message: Unknown partner client
              requestId: ''
    PartnerAccountNotFound:
      description: |
        No link is keyed to this `partnerUserId`. This is the normal answer
        for an account that has never been connected — treat it as "not
        linked", not as an error.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: PARTNER_ACCOUNT_NOT_FOUND
              message: No link found for partnerUserId
              requestId: request_sandbox_204
    ServerError:
      description: |
        A Bilt-side problem — `PARTNER_MISCONFIGURED` or `INTERNAL_ERROR`,
        or one of the "codes you should not see" in the guide's
        [§9.2](/account-linking/account-links-api#9-2-error-code-reference).
        Not fixable by retrying — report it with the `requestId`.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: INTERNAL_ERROR
              message: Unexpected server error
              requestId: request_sandbox_214
    IdpUnavailable:
      description: |
        Bilt could not reach its own IdP to validate a token — the `idToken`
        on `oauth-link`, or your partner access token on any endpoint. Retry
        with backoff, resending the identical request. When it is your
        credential being validated, the response is raised before the
        correlation step and carries an empty `requestId` (guide §9.1); on
        `ciba-callback` nothing was settled and the token is not spent.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            idTokenValidation:
              summary: oauth-link — validating the idToken
              value:
                error:
                  code: IDP_UNAVAILABLE
                  message: Bilt IdP unreachable during id_token validation
                  requestId: request_sandbox_213
            partnerTokenValidation:
              summary: any endpoint — validating your partner access token
              value:
                error:
                  code: IDP_UNAVAILABLE
                  message: Bilt IdP unreachable during partner token validation
                  requestId: ''
  securitySchemes:
    partnerBearer:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >
        **Preferred.** A partner access token minted by **your partner

        client** with the client-credentials grant — the client you

        authenticate **as**, with no member involved. Send it as

        `Authorization: Bearer <access_token>` and on no other header. It is

        a different client from the IdP one that mints member `id_token`s,

        and it is not the member `access_token` the code exchange returns —

        see the guide's

        [§5
        Authentication](/account-linking/account-links-api#5-authentication).
    apiKey:
      type: apiKey
      in: header
      name: x-api-key
      description: |
        **Being retired.** The older per-partner credential, still issued at
        onboarding and still accepted; the partner client is what
        authenticates these calls once you adopt it. Do not send it alongside
        a bearer token — a rejected token is never rescued by a valid key.

````