openapi: 3.2.0 info: title: Bilt Checkout SDK One-time payments API version: 0.2.0-draft description: '# Introduction External backend contract for integrators using the Bilt Checkout SDK.' 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: - checkoutPartnerOAuth: [] tags: - name: One-Time Payments description: 'Open a checkout session that renders Bilt-hosted checkout in the Checkout SDK. The customer confirms the payment themselves; the outcome arrives as a `checkout.session.*` webhook.' paths: /partner/checkout/v1/checkout-sessions: post: tags: - One-Time Payments summary: Create a one-time payment checkout session operationId: createCheckoutSession description: 'Creates a hosted checkout session for one order and returns a short-lived `checkoutUrl` to present in the Checkout SDK. Bilt resolves `integratorCustomerId` to the linked Bilt member and renders their payment methods, eligible credits, and the order summary inside the hosted checkout — your app renders none of this. The session stays `OPEN` until the customer pays, abandons it, or it reaches `expiresAt`; each of those outcomes is delivered as a `checkout.session.*` webhook. Session TTL is configured per integrator during onboarding. `201` means the session exists. It does not mean the customer has paid — wait for `checkout.session.completed`.' parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreatePaymentRequest' example: integratorCustomerId: acme_customer_sandbox_123 orderId: acme_order_sandbox_456 lineItems: - integratorSkuId: acme_membership_monthly lineItemType: PRODUCT quantity: 1 unitPriceCents: 4900 amountCents: 4900 currency: USD metadata: correlationId: acme_correlation_sandbox_789 responses: '201': description: Checkout session created content: application/json: schema: $ref: '#/components/schemas/CheckoutSessionEnvelope' example: data: sessionId: checkout_session_sandbox_123 mode: hosted status: OPEN orderId: acme_order_sandbox_456 amounts: subtotalCents: 4900 currency: USD checkoutUrl: https://checkout.sandbox.example.invalid/s/session_sandbox_token expiresAt: '2026-08-24T22:30:00Z' '400': $ref: '#/components/responses/InvalidRequest' '401': $ref: '#/components/responses/Unauthenticated' '404': $ref: '#/components/responses/CustomerNotFound' '409': $ref: '#/components/responses/Conflict' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/ServiceUnavailable' callbacks: checkoutSessionCompleted: https://acme.example.invalid/webhooks/bilt: post: tags: - Payment webhooks summary: checkout.session.completed operationId: onCheckoutSessionCompleted security: [] parameters: - $ref: '#/components/parameters/WebhookId' - $ref: '#/components/parameters/WebhookTimestamp' - $ref: '#/components/parameters/WebhookSignature' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/PaymentWebhookEvent' example: type: checkout.session.completed timestamp: '2026-08-24T22:15:12.344Z' data: sessionId: checkout_session_sandbox_123 mode: hosted orderId: acme_order_sandbox_456 integratorCustomerId: acme_customer_sandbox_123 status: COMPLETED paymentId: payment_sandbox_789 amounts: subtotalCents: 4900 creditsAppliedCents: 500 chargedAmountCents: 4400 currency: USD responses: 2xx: description: Event acknowledged; Bilt will not redeliver it. checkoutSessionCancelledOrExpired: https://acme.example.invalid/webhooks/bilt: post: tags: - Payment webhooks summary: checkout.session.cancelled / checkout.session.expired operationId: onCheckoutSessionEnded security: [] parameters: - $ref: '#/components/parameters/WebhookId' - $ref: '#/components/parameters/WebhookTimestamp' - $ref: '#/components/parameters/WebhookSignature' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/PaymentWebhookEvent' examples: cancelled: value: type: checkout.session.cancelled timestamp: '2026-08-24T22:20:00.000Z' data: sessionId: checkout_session_sandbox_123 mode: hosted orderId: acme_order_sandbox_456 integratorCustomerId: acme_customer_sandbox_123 status: CANCELLED expired: value: type: checkout.session.expired timestamp: '2026-08-24T22:30:00.000Z' data: sessionId: checkout_session_sandbox_123 mode: hosted orderId: acme_order_sandbox_456 integratorCustomerId: acme_customer_sandbox_123 status: EXPIRED responses: 2xx: description: Event acknowledged; Bilt will not redeliver it. components: parameters: WebhookId: name: webhook-id in: header required: true description: 'Unique message id, stable across redeliveries of the same event. Deduplicate on it. ' schema: type: string example: msg_2KWPBgLlAfxdpx2AI54pPJ85f4W IdempotencyKey: name: Idempotency-Key in: header required: false description: 'Optional but strongly recommended — required in practice for headless payments. Any string up to 255 characters; a fresh UUID per attempt is the recommended form. A retry with the same key and body returns the original result; the same key with a different body is rejected with `409 IDEMPOTENCY_CONFLICT`. Keys are scoped to your client. ' schema: type: string minLength: 1 maxLength: 255 example: 6f6b1afd-22e0-4137-a096-b15e41cdc890 WebhookSignature: name: webhook-signature in: header required: true description: 'Space-separated `v1,` signatures, each the base64 `HMAC-SHA256` of `"{webhook-id}.{webhook-timestamp}.{rawBody}"` keyed by your `whsec_` signing secret. Accept the delivery if any one verifies (constant-time comparison). Multiple entries appear only during a secret rotation. ' schema: type: string example: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4= WebhookTimestamp: name: webhook-timestamp in: header required: true description: 'Unix timestamp in seconds of this delivery attempt. Reject deliveries more than 5 minutes from your clock. ' schema: type: integer format: int64 example: 1787609712 schemas: PaymentWebhookEvent: description: 'Standard Webhooks payload. `type` is the discriminator: each event type fixes `data.mode` and `data.status` and states exactly which fields are present, so a validator generated from this document rejects e.g. `payment.failed` without `failure` or `payment.succeeded` without settled `amounts`. ' oneOf: - $ref: '#/components/schemas/CheckoutSessionCompletedEvent' - $ref: '#/components/schemas/CheckoutSessionCancelledEvent' - $ref: '#/components/schemas/CheckoutSessionExpiredEvent' - $ref: '#/components/schemas/PaymentSucceededEvent' - $ref: '#/components/schemas/PaymentFailedEvent' discriminator: propertyName: type mapping: checkout.session.completed: '#/components/schemas/CheckoutSessionCompletedEvent' checkout.session.cancelled: '#/components/schemas/CheckoutSessionCancelledEvent' checkout.session.expired: '#/components/schemas/CheckoutSessionExpiredEvent' payment.succeeded: '#/components/schemas/PaymentSucceededEvent' payment.failed: '#/components/schemas/PaymentFailedEvent' CheckoutSessionCompletedEvent: allOf: - $ref: '#/components/schemas/WebhookEventBase' - type: object properties: type: type: string enum: - checkout.session.completed data: allOf: - $ref: '#/components/schemas/WebhookDataBase' - type: object required: - paymentId - amounts properties: mode: type: string enum: - hosted status: type: string enum: - COMPLETED paymentId: type: string description: Opaque Bilt payment identifier. example: payment_sandbox_789 amounts: $ref: '#/components/schemas/PaymentAmounts' WebhookEventBase: type: object required: - type - timestamp - data properties: type: type: string description: Event type; see **Introduction › Webhooks › Payload**. timestamp: type: string format: date-time description: When the event occurred (not when it was delivered). example: '2026-08-24T22:15:12.344Z' WebhookDataBase: type: object required: - sessionId - mode - orderId - integratorCustomerId - status properties: sessionId: type: string description: The `sessionId` returned when the session or payment was created. example: checkout_session_sandbox_123 orderId: type: string description: Your `orderId`, echoed unchanged. example: acme_order_sandbox_456 integratorCustomerId: type: string description: Your customer identifier, echoed unchanged. example: acme_customer_sandbox_123 metadata: $ref: '#/components/schemas/IntegratorMetadata' IntegratorMetadata: type: - object - 'null' maxProperties: 20 additionalProperties: type: string maxLength: 500 description: 'Optional integrator-owned correlation values (up to 20 keys, string values up to 500 characters). Echoed back on webhooks and never interpreted by Bilt. Do not include secrets, credentials, card data, or personal data. ' example: correlationId: acme_correlation_sandbox_789 ExternalLineItem: type: object additionalProperties: false required: - integratorSkuId - lineItemType - quantity - unitPriceCents - amountCents - currency properties: integratorSkuId: type: string minLength: 1 maxLength: 255 description: 'Your identifier for the SKU; Bilt maps it to its catalog. Unique within one request. [CHECK: the current implementation accepts this field as `integratorSku`; rename pending.] ' example: acme_membership_monthly lineItemType: type: string enum: - PRODUCT description: 'Kind of line. `PRODUCT` today. [CHECK: `SHIPPING`, `SUBSCRIPTION`, `INCIDENTAL` are under discussion and not yet accepted.] ' example: PRODUCT quantity: type: integer minimum: 1 example: 1 unitPriceCents: type: integer format: int64 minimum: 0 description: 'Price per unit in minor currency units. May be `0` for a bundled or promotional line; never negative — a discount is not a line item. ' example: 4900 amountCents: type: integer format: int64 minimum: 0 description: 'Line total in minor units; must equal `quantity × unitPriceCents` or the request is rejected with `400 INVALID_REQUEST`. ' example: 4900 currency: type: string pattern: ^[A-Z]{3}$ description: ISO 4217 currency code. Currently only `USD` is accepted. example: USD PaymentFailure: type: object additionalProperties: false description: Why a headless charge could not be completed. required: - code - message - retryable properties: code: type: string pattern: ^[A-Z][A-Z0-9_]*$ description: 'Stable reason code. `CARD_DECLINED`, `PAYMENT_METHOD_EXPIRED`, `PAYMENT_METHOD_NOT_FOUND`, `PROCESSOR_UNAVAILABLE`. [CHECK: final failure-code list is owned by the payment processor integration.] ' example: CARD_DECLINED message: type: string description: Human-readable reason; do not branch on it. example: The card issuer declined the charge retryable: type: boolean description: 'Whether a **new** headless payment for the same order may succeed without the customer acting first (for example a transient processor failure). `false` means the customer needs to update their payment method in the Bilt app. ' example: true PaymentAmounts: type: object required: - subtotalCents - creditsAppliedCents - chargedAmountCents - currency description: 'Split of the order total between eligible Bilt credits and the customer''s card. `subtotalCents = creditsAppliedCents + chargedAmountCents`. On a `202` acceptance `creditsAppliedCents` is `0`; the final split is on the `payment.succeeded` webhook. A synchronous `200` response carries the final split. ' properties: subtotalCents: type: integer format: int64 minimum: 0 description: Sum of `amountCents` over all line items. example: 4900 creditsAppliedCents: type: integer format: int64 minimum: 0 description: Eligible Bilt credits applied automatically. example: 500 chargedAmountCents: type: integer format: int64 minimum: 0 description: Amount charged to the customer's payment method. example: 4400 currency: type: string pattern: ^[A-Z]{3}$ example: USD CheckoutSession: type: object required: - sessionId - mode - status - orderId - amounts - checkoutUrl - expiresAt properties: sessionId: type: string description: Opaque Bilt checkout-session identifier; echoed on webhooks. example: checkout_session_sandbox_123 mode: type: string enum: - hosted status: type: string enum: - OPEN description: 'Always `OPEN` on creation. Terminal states (`COMPLETED`, `CANCELLED`, `EXPIRED`) are delivered by webhook. ' orderId: type: string description: Your `orderId`, echoed unchanged. example: acme_order_sandbox_456 amounts: description: 'Order total as submitted. Credits are chosen by the customer inside the hosted checkout, so the final split arrives on `checkout.session.completed`. ' $ref: '#/components/schemas/OrderAmounts' checkoutUrl: type: string format: uri description: 'Short-lived handoff URL to present in the Checkout SDK. Treat it as a bearer credential for this one session. ' example: https://checkout.sandbox.example.invalid/s/session_sandbox_token expiresAt: type: string format: date-time description: When the session and its `checkoutUrl` stop working. example: '2026-08-24T22:30:00Z' PaymentSucceededEvent: allOf: - $ref: '#/components/schemas/WebhookEventBase' - type: object properties: type: type: string enum: - payment.succeeded data: allOf: - $ref: '#/components/schemas/WebhookDataBase' - type: object required: - paymentId - amounts properties: mode: type: string enum: - headless status: type: string enum: - SUCCEEDED paymentId: type: string description: Opaque Bilt payment identifier. example: payment_sandbox_790 amounts: $ref: '#/components/schemas/PaymentAmounts' CheckoutSessionCancelledEvent: allOf: - $ref: '#/components/schemas/WebhookEventBase' - type: object properties: type: type: string enum: - checkout.session.cancelled data: allOf: - $ref: '#/components/schemas/WebhookDataBase' - type: object properties: mode: type: string enum: - hosted status: type: string enum: - CANCELLED ErrorResponse: type: object additionalProperties: false required: - error description: 'Error envelope returned on every non-`2xx` response. Branch on the stable `code`, not the human-readable `message`. ' properties: error: $ref: '#/components/schemas/ErrorDetail' CheckoutSessionExpiredEvent: allOf: - $ref: '#/components/schemas/WebhookEventBase' - type: object properties: type: type: string enum: - checkout.session.expired data: allOf: - $ref: '#/components/schemas/WebhookDataBase' - type: object properties: mode: type: string enum: - hosted status: type: string enum: - EXPIRED PaymentFailedEvent: allOf: - $ref: '#/components/schemas/WebhookEventBase' - type: object properties: type: type: string enum: - payment.failed data: allOf: - $ref: '#/components/schemas/WebhookDataBase' - type: object required: - failure properties: mode: type: string enum: - headless status: type: string enum: - FAILED failure: $ref: '#/components/schemas/PaymentFailure' OrderAmounts: type: object required: - subtotalCents - currency properties: subtotalCents: type: integer format: int64 minimum: 0 description: Sum of `amountCents` over all line items. example: 4900 currency: type: string pattern: ^[A-Z]{3}$ example: USD ErrorDetail: type: object additionalProperties: false required: - code - message - requestId - retryable properties: code: type: string pattern: ^[A-Z][A-Z0-9_]*$ description: Stable machine-readable code; see **Introduction › Errors**. enum: - INVALID_REQUEST - UNAUTHENTICATED - CUSTOMER_NOT_FOUND - PAYMENT_METHOD_NOT_FOUND - IDEMPOTENCY_CONFLICT - SESSION_ALREADY_COMPLETED - RATE_LIMIT_EXCEEDED - INTERNAL_ERROR - PAYMENT_UNAVAILABLE example: CUSTOMER_NOT_FOUND message: type: string description: Human-readable operational message; do not branch on it. example: No linked Bilt member for integratorCustomerId requestId: type: string description: Correlation id to quote when contacting Bilt. example: request_sandbox_123 retryable: type: boolean description: 'Whether sending the same request again may succeed. `false` whenever somebody has to act first. ' example: false CreatePaymentRequest: type: object additionalProperties: false description: 'Shared request body for one-time payment sessions and headless payments. Unknown properties are rejected with `400 INVALID_REQUEST`. ' required: - integratorCustomerId - orderId - lineItems properties: integratorCustomerId: type: string minLength: 1 maxLength: 64 pattern: ^[A-Za-z0-9_.:~-]+$ description: 'Your stable identifier for the customer — the same key you use for account linking. Bilt resolves it to the linked Bilt member. Never send a Bilt member id. ' example: acme_customer_sandbox_123 orderId: type: string minLength: 1 maxLength: 255 pattern: ^[A-Za-z0-9_.:~-]+$ description: 'Your identifier for this order, invoice, or payment attempt. It must exist on your side before you call this endpoint — Bilt only stores and echoes it, unchanged, on the response and on every webhook for this payment. If your order record is created only after payment, generate the id first and attach the order to it when the webhook arrives. ' example: acme_order_sandbox_456 lineItems: type: array minItems: 1 maxItems: 100 description: 'Sealed list of what is being paid for. All items must share one `currency`, and `integratorSkuId` must be unique within the request. ' items: $ref: '#/components/schemas/ExternalLineItem' metadata: $ref: '#/components/schemas/IntegratorMetadata' CheckoutSessionEnvelope: type: object required: - data properties: data: $ref: '#/components/schemas/CheckoutSession' responses: InvalidRequest: description: 'The request is malformed. `message` names the offending field. Not retryable without changing the request. ' content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: invalidField: value: error: code: INVALID_REQUEST message: lineItems[0].quantity must be at least 1 requestId: request_sandbox_123 retryable: false unknownField: value: error: code: INVALID_REQUEST message: Unrecognized field "paymentMethodId" requestId: request_sandbox_124 retryable: false mixedCurrency: value: error: code: INVALID_REQUEST message: All lineItems must use the same currency; only USD is supported requestId: request_sandbox_125 retryable: false RateLimited: description: 'Too many requests. Gateway-enforced; retry with backoff, honouring `Retry-After` when present. The sync endpoint''s limit is lower than the async endpoint''s. Per-integrator quotas are agreed during onboarding when your volume warrants one. ' headers: Retry-After: description: Number of seconds to wait before retrying. schema: type: integer minimum: 1 content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: code: RATE_LIMIT_EXCEEDED message: Too many requests requestId: request_sandbox_132 retryable: true InternalError: description: 'Unexpected Bilt error. For headless payments, do not retry blindly: the charge may have been accepted — check for a `payment.*` webhook for the same `orderId` first, and retry with the **same** `Idempotency-Key`. ' content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: code: INTERNAL_ERROR message: Unexpected server error requestId: request_sandbox_133 retryable: false Conflict: description: Idempotency conflict, or the session is already terminal. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: idempotencyConflict: value: error: code: IDEMPOTENCY_CONFLICT message: Idempotency-Key was already used with a different request body requestId: request_sandbox_130 retryable: false sessionAlreadyCompleted: value: error: code: SESSION_ALREADY_COMPLETED message: The checkout session is already in a terminal state requestId: request_sandbox_131 retryable: false CustomerNotFound: description: '`integratorCustomerId` is not linked to a Bilt member. Send the customer through account linking, then retry. ' content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: code: CUSTOMER_NOT_FOUND message: No linked Bilt member for integratorCustomerId requestId: request_sandbox_127 retryable: false ServiceUnavailable: description: 'Bilt could not reach the customer''s wallet or another required dependency. Nothing was charged. Retry with backoff. ' content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: code: PAYMENT_UNAVAILABLE message: Payment could not be attempted; try again shortly requestId: request_sandbox_134 retryable: true Unauthenticated: description: Missing, expired, or wrong-audience access token. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: code: UNAUTHENTICATED message: Missing or invalid access token requestId: request_sandbox_126 retryable: false securitySchemes: checkoutPartnerOAuth: type: oauth2 description: 'OAuth 2.0 client credentials against the `enterprise-partner` Keycloak realm. Bilt issues your client id and client secret during onboarding; tokens carry an audience Bilt assigns to your client at onboarding, and Bilt identifies your integration from the token''s `azp` claim. You do not request the audience or any scope yourself: Bilt assigns the audience to your client at onboarding, and no scopes are required — a plain client-credentials grant with your client id and secret is enough. The values you need from Bilt are the token endpoint for the environment, your client id, and your client secret. Send the result as `Authorization: Bearer `. The token URL below is **staging** — production is `https://www.bilt.com/realms/enterprise-partner/protocol/openid-connect/token`. Keep the client secret server-side only. ' flows: clientCredentials: tokenUrl: https://staging.biltrewards.com/realms/enterprise-partner/protocol/openid-connect/token scopes: {} x-tagGroups: - name: Checkout SDK tags: - Integration models - Gateway mode - Choosing an acquirer - One-time payments - Headless payments - Default payment method - Payment webhooks