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

# Create a link

> Link a Bilt member to your account with an id_token from the Bilt IdP.

Call this from your backend after the OAuth code exchange. See [Link from your app](/partners/account-linking/link-from-your-app.md).


## OpenAPI

````yaml /partners/openapi.yaml post /v1/account-links/oauth-link
openapi: 3.1.0
info:
  title: Bilt partner API
  version: 1.0.0
  license:
    name: Proprietary
    identifier: LicenseRef-Proprietary
  description: >
    Link a Bilt member to an account in your product, and report that member's
    orders so Bilt can reward them. The `paths` are the endpoints Bilt hosts.
    The `webhooks` are the endpoints you host so Bilt can link, create, and
    unlink an account from the Bilt app.
  contact:
    name: Bilt partner support
    url: https://docs.bilt.dev/partners/overview
servers:
  - url: https://partnerapi.biltrewards.com
    description: Production
  - url: https://staging.partnerapi.biltrewards.com
    description: Staging
security:
  - partnerBearer: []
  - apiKey: []
tags:
  - name: Endpoints you host
    description: >
      Endpoints you build and host. Bilt calls them from its backend when a
      member links, creates, or unlinks an account from the Bilt app. Base URLs
      are agreed during onboarding.
  - name: Account links
    description: >
      Endpoints Bilt hosts. You call them from your backend to create, read, and
      end a link.
  - name: Order reports
    description: >
      Endpoints Bilt hosts on its webhooks host. You post each change to a
      linked customer's order, signed with your Ed25519 key. This contract is a
      proposal under design review and is not live yet.
paths:
  /v1/account-links/oauth-link:
    post:
      tags:
        - Account links
      summary: Create a link
      description: >
        Links the Bilt member named by the `id_token` to your account
        `partnerUserId`. The response is the new link with `linkStatus: LINKED`.
        Call it as soon as the token exchange returns: the token is short-lived
        and single-use.
      operationId: partnerOauthLink
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OAuthLinkRequest'
            example:
              idToken: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
              partnerUserId: p-7f3a9c
      responses:
        '200':
          description: >-
            Link created. A byte-identical retry of a request that already
            succeeded returns the same link.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Link'
              example:
                linkId: 3b1f6a52-9d0e-4c8a-a1f4-2e6d7c9b8a10
                partnerUserId: p-7f3a9c
                linkStatus: LINKED
                linkedAt: '2026-05-01T14:03:22Z'
                partnerDetails: {}
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          description: >
            `UNAUTHORIZED`: your credential is missing, unknown, or disabled.
            `INVALID_ID_TOKEN`: the token failed signature, issuer, or subject
            validation, or Bilt could not verify it against its IdP.
            `ID_TOKEN_EXPIRED`: the token is past its expiry.
            `ID_TOKEN_AUDIENCE_MISMATCH`: the token was minted for a different
            OAuth client.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: ID_TOKEN_EXPIRED
                  message: id_token is expired
                  requestId: 4bf92f3577b34da6a3ce929d0e0e4736
        '409':
          description: >
            `ID_TOKEN_REPLAYED`: the token was already used for a different
            link. `ALREADY_LINKED`: the member is linked to a different account
            of yours. `IDENTITY_ALREADY_LINKED`: the account is linked to a
            different Bilt member. `CONCURRENT_UPDATE`: two writes raced; resend
            the identical request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: ALREADY_LINKED
                  message: Member is already linked to a different partner account
                  requestId: 4bf92f3577b34da6a3ce929d0e0e4736
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '500':
          $ref: '#/components/responses/ServerError'
        '502':
          $ref: '#/components/responses/IdpUnavailable'
components:
  schemas:
    OAuthLinkRequest:
      type: object
      required:
        - idToken
        - partnerUserId
      properties:
        idToken:
          type: string
          minLength: 1
          maxLength: 8192
          description: >
            The `id_token` from your code exchange with the Bilt IdP, sent as
            issued. Its `sub` names the Bilt member. Do not decode, rewrite, or
            reuse it.
        partnerUserId:
          type: string
          minLength: 1
          maxLength: 256
          description: >
            Your stable, opaque id for the account. It is the only handle for
            the link afterwards. Do not put personal data in it.
    Link:
      type: object
      required:
        - linkId
        - partnerUserId
        - linkStatus
      properties:
        linkId:
          type: string
          format: uuid
          description: The id of this link episode. Store it; you send it to end the link.
        partnerUserId:
          type: string
          description: Your account id.
        linkStatus:
          $ref: '#/components/schemas/LinkStatus'
        linkedAt:
          type: string
          format: date-time
          description: When the link was created.
        partnerDetails:
          type: object
          additionalProperties: true
          description: Reserved. Currently always an empty object.
    ErrorResponse:
      type: object
      description: The error envelope for business failures on the Bilt-hosted endpoints.
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              $ref: '#/components/schemas/ErrorCode'
            message:
              type: string
              description: For your logs only. Never show it to a customer.
            requestId:
              type: string
              description: >
                The trace id of the request. Log it and quote it to Bilt
                support. Absent when the request had no active trace, and empty
                on credential refusals. An `x-request-id` you send is not
                echoed.
            source:
              type: string
              description: The Bilt service that produced the error.
            retry:
              type: boolean
              description: Always `false`. Decide retries from `code`.
            customProperties:
              type: object
              additionalProperties: true
            humanReadableMessage:
              type: boolean
              description: Always `false`.
    LinkStatus:
      type: string
      description: >
        `LINKED` is the only usable state. `UNLINKED` means the member or Bilt
        support ended the link. `LINK_REVOKED` means you ended it.
        `UNLINKING_IN_PROGRESS` means Bilt is calling you to end it: treat it as
        unusable but not final, and read it again. A new link after `UNLINKED`
        or `LINK_REVOKED` creates a fresh link.
      enum:
        - LINKED
        - UNLINKED
        - LINK_REVOKED
        - UNLINKING_IN_PROGRESS
    ValidationErrorResponse:
      type: object
      description: >-
        The shape of a `400` when the request fails validation. It has no
        `error.code`.
      required:
        - title
        - status
        - violations
      properties:
        title:
          type: string
          examples:
            - Constraint Violation
        status:
          type: integer
          examples:
            - 400
        violations:
          type: array
          items:
            type: object
            properties:
              field:
                type: string
                examples:
                  - unlink.request.partnerUserId
              message:
                type: string
                examples:
                  - must not be blank
    ErrorCode:
      type: string
      description: >-
        Every code the Bilt-hosted endpoints return. Branch on this, not on the
        message.
      enum:
        - UNAUTHORIZED
        - INVALID_ID_TOKEN
        - ID_TOKEN_EXPIRED
        - ID_TOKEN_AUDIENCE_MISMATCH
        - PARTNER_ACCOUNT_NOT_FOUND
        - LINK_NOT_FOUND
        - ALREADY_LINKED
        - IDENTITY_ALREADY_LINKED
        - ID_TOKEN_REPLAYED
        - CONCURRENT_UPDATE
        - INVALID_STATE_TRANSITION
        - IDP_UNAVAILABLE
        - PARTNER_MISCONFIGURED
        - INTERNAL_ERROR
        - ACCOUNT_LINK_PERSISTENCE_ERROR
        - INTERNAL_SERVER_ERROR
        - PARTNER_DISABLED
  responses:
    BadRequest:
      description: >
        The body or path failed validation and returns the validation shape.
        Malformed JSON returns neither shape, so check the status code first.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ValidationErrorResponse'
    UnsupportedMediaType:
      description: 'The request did not send `Content-Type: application/json`.'
    ServerError:
      description: >
        `PARTNER_MISCONFIGURED`: a Bilt-side configuration problem.
        `INTERNAL_ERROR`: an unexpected Bilt-side failure. Neither is fixed by
        retrying. Report it with the `requestId`.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    IdpUnavailable:
      description: >
        `IDP_UNAVAILABLE`: Bilt's IdP was unreachable while validating a token.
        Retry with backoff. When it was your access token that could not be
        validated, the `requestId` is empty.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
  securitySchemes:
    partnerBearer:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >
        Preferred. An access token your partner client mints with the client
        credentials grant. Send it only on the `Authorization` header. It is not
        the member access token from the code exchange.
    apiKey:
      type: apiKey
      in: header
      name: x-api-key
      description: >
        Being retired in favor of the partner access token. Do not send both: a
        rejected bearer token does not fall back to the key.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.