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

# Break a link

> Ends the link named by `linkId`. Call this when a customer disconnects
Bilt inside **your** product, or when you close their account. It is
an API call you make, not a webhook Bilt subscribes to. The link moves
to `LINK_REVOKED`.

**`linkId` names which link you are ending, and it is required.** One
of your accounts can hold several links over time — a customer
disconnects and reconnects, and each episode is kept rather than
deleted. Without the id, an unlink delayed in your retry queue would
land on whichever link happens to be current, ending one the customer
has since re-created. You do not have to store it: Bilt returns
`linkId` on the `oauth-link` response and on the status read.

**Idempotent.** Unlinking a link that is already ended is `204`, not
an error, so retry on a lost response however late — the retry ends
the link it was issued for and never the new one.

`409 INVALID_STATE_TRANSITION` means the link was **busy**, not that
your request was wrong. It is only possible for integrations where
Bilt calls you back on unlink: the member disconnects in the Bilt
app, Bilt claims the link and calls you, and your own `POST /unlink`
arrives inside that window. Back off a second or two and re-send the
identical request.




## OpenAPI

````yaml /account-linking/account-links-openapi.yaml post /v1/account-links/unlink
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/unlink:
    post:
      tags:
        - Unlinking
      summary: Break a link
      description: |
        Ends the link named by `linkId`. Call this when a customer disconnects
        Bilt inside **your** product, or when you close their account. It is
        an API call you make, not a webhook Bilt subscribes to. The link moves
        to `LINK_REVOKED`.

        **`linkId` names which link you are ending, and it is required.** One
        of your accounts can hold several links over time — a customer
        disconnects and reconnects, and each episode is kept rather than
        deleted. Without the id, an unlink delayed in your retry queue would
        land on whichever link happens to be current, ending one the customer
        has since re-created. You do not have to store it: Bilt returns
        `linkId` on the `oauth-link` response and on the status read.

        **Idempotent.** Unlinking a link that is already ended is `204`, not
        an error, so retry on a lost response however late — the retry ends
        the link it was issued for and never the new one.

        `409 INVALID_STATE_TRANSITION` means the link was **busy**, not that
        your request was wrong. It is only possible for integrations where
        Bilt calls you back on unlink: the member disconnects in the Bilt
        app, Bilt claims the link and calls you, and your own `POST /unlink`
        arrives inside that window. Back off a second or two and re-send the
        identical request.
      operationId: partnerUnlink
      parameters:
        - $ref: '#/components/parameters/XRequestId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UnlinkRequest'
            example:
              partnerUserId: acme_customer_sandbox_123
              linkId: 9f1c2e4a-7b30-4d55-8e21-6c0b9a3f5d78
      responses:
        '204':
          description: Link revoked — or already terminal (idempotent retry). No body.
        '400':
          $ref: '#/components/responses/ValidationFailure'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/LinkNotFound'
        '409':
          $ref: '#/components/responses/UnlinkConflict'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '500':
          $ref: '#/components/responses/ServerError'
        '502':
          $ref: '#/components/responses/IdpUnavailable'
components:
  parameters:
    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:
    UnlinkRequest:
      type: object
      required:
        - partnerUserId
        - linkId
      properties:
        partnerUserId:
          type: string
          minLength: 1
          maxLength: 256
          description: The account whose link you are ending.
          example: acme_customer_sandbox_123
        linkId:
          type: string
          format: uuid
          description: |
            The id of the link episode being ended, as returned on the
            `oauth-link` response and on the status read. Required. An id
            that names nothing you may act on answers `404`, exactly as an id
            that exists nowhere.
          example: 9f1c2e4a-7b30-4d55-8e21-6c0b9a3f5d78
    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: ''
    LinkNotFound:
      description: |
        No link of yours has that `linkId`. The same answer whether the id
        never existed, belongs to one of your other accounts, or belongs to
        another partner — a `404` is never a hint that an id is real
        somewhere else.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: LINK_NOT_FOUND
              message: No link with that id for this partner account
              requestId: request_sandbox_205
    UnlinkConflict:
      description: |
        `INVALID_STATE_TRANSITION`: the link was mid-transition (Bilt is
        calling you to end it) — back off a second and resend the identical
        request. `CONCURRENT_UPDATE`: two writes raced — resend.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            invalidStateTransition:
              value:
                error:
                  code: INVALID_STATE_TRANSITION
                  message: Link is UNLINKING_IN_PROGRESS
                  requestId: request_sandbox_210
            concurrentUpdate:
              value:
                error:
                  code: CONCURRENT_UPDATE
                  message: Concurrent update on the same link
                  requestId: request_sandbox_211
    UnsupportedMediaType:
      description: |
        The request body was missing or sent with a `Content-Type` other than
        `application/json`. Framework-level response, no body.
    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.

````