> This page is for version v1 (default).
> For other versions, use one of these documentation indexes:
> - v1 (default): https://developers.truemed.com/v-1/llms.txt

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://developers.truemed.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://developers.truemed.com/_mcp/server.

# Create Payment Session

POST https://dev-api.truemed.com/payments/v1/create_payment_session
Content-Type: application/json

Create a payment session to begin the "Pay with Truemed" checkout flow. Call this endpoint after the customer clicks "Checkout with Truemed" or similar. The customer is redirected to Truemed to complete a health survey and enter HSA/FSA card details, then redirected back to your site via the provided `success_url` or `failure_url`.

The outcome is communicated asynchronously via the
[`payment_session_complete`](/api-reference/payment-sessions/payment-session-completed-webhook) webhook.

## Request Notes

* `total_amount` **(Required)**: The total amount in cents to charge the user, inclusive of discounts, shipping, taxes, and other fees being collected by the partner
  * **IMPORTANT**: total\_amount must equal the sum of:
    * `payment.amount_details.total_additional_charges`
    * `payment.amount_details.total_discounts`
    * For each order\_item, the sum of:
      * `item.quantity * item.price`
      * `item.amount_details.total_additional_charges`
      * `item.amount_details.total_discounts`
* `amount_details` **(Required)**: [Read further here](/guides/payment-sessions/itemized-fees-and-discounts)
* `success_url` **(Required)**: Where to redirect the user to after Truemed successfully processes a payment
  * This is often used to display an order confirmation screen, or similar
  * **Templating**:
    * We support limited templating of this URL parameter. Any part of the URL string containing `{{payment_session_id}}` will be replaced by the `id` (UUID) returned by this call.
    * **IMPORTANT**: The template variable must be EXACTLY `{{payment_session_id}}` - no spaces around the braces on either side.
  * For unattended payment flows using payment tokens, the meaning of the `success_url` is slightly different. In such cases a payment may fail because the stored payment methods are expired or otherwise declined. When those failures occur, we will return a 422 status with a `redirect_url` to our page for updating stored payment methods. Merchants should email or otherwise notify users of the need to update their stored payment methods using this url. When users successfully complete that page, they will be redirected to the `success_url` provided here.
* `failure_url` **(Required)**: When the payment fails in an irrecoverable way, or the user is ineligible to spend HSA/FSA funds on the purchase
  * We attempt to recover from incorrect CVV, or other card detail errors by prompting the user for retry
  * The exact failure reason will be communicated in the call to the partner's `payment_session_complete` callback
  * **Templating**:
    * We support limited templating of this URL parameter. Any part of the URL string containing `{{payment_session_id}}` will be replaced by the `id` (UUID) returned by this call.
    * **IMPORTANT**: The template variable must be EXACTLY `{{payment_session_id}}` - no spaces around the braces on either side.
  * For unattended payment flows using payment tokens, the meaning of the `failure_url` is slightly different. In such cases a payment may fail because the stored payment methods are expired or otherwise declined. When those failures occur, we will return a 422 status with a `redirect_url` to our page for updating stored payment methods. Merchants should email or otherwise notify users of the need to update their stored payment methods using this url. When users start but cancel the update before fully completing the process, they will be redirected to the `failure_url` provided here.
* `customer_state` **(Optional)**: The 2-letter ISO 3166-2:US code for a region, *without the* US- *prefix*. For example, California should be provided as CA and Rhode Island as RI
  * If `customer_state` is provided, the state of residence question in the health survey will be prefilled with the full state name that corresponds to the ISO code.
* `metadata` **(Optional)**: - Will be echoed back in the webhook and detail view if present.
  * **IMPORTANT**: This must be a string. If you want to include JSON, encode it as a string.
  * **IMPORTANT**: DO NOT INCLUDE sensitive information in this field.
* `kind` **(Optional)**: one of the following:
  * `one_time_payment` **(DEFAULT)**: The charge is not a subscription related charge. This is the default if this field is not set.
  * `authorization`: A `one_time_payment` authorization hold only, must be manually captured or voided with a follow up API call.
* `payment_token` **(Conditionally required)**: the ID returned by Truemed on initial recurring charges (when `tokenize=True`).
  * When `payment_token` is provided, customer identity is derived from the token's user. Any `customer_email` or `customer_name` values in the request are ignored.
* `tokenize` **(Optional)**: `True` or `False` - set to `True` to enable subsequent charges. Must be `False` if `payment_token` is provided.
* `request_multicapture` **(Optional)**: `True` or `False` - set to `True` to opt this authorization into [multiple partial captures](/guides/setup/api/payment-sessions/manual-delayed-capture#capturing-in-multiple-parts).
  * **IMPORTANT**: This field requires multi-capture to be enabled on your sales channel, and cannot be combined with `tokenize=true` or `payment_token`. Otherwise the request fails with `FeatureNotEnabled`.
  * Only meaningful with `kind=authorization`. On a `one_time_payment` the charge captures immediately, so the flag is accepted but does nothing.
  * Requesting multi-capture does not guarantee it. The card network decides at authorization time, so a later capture sent with `final: false` can still be rejected.
* `display_next_bill_info` **(Optional)**: Partners should only provide this field if they want to initiate a free trial subscription for the customer. The information in this object will be displayed on Truemed's checkout page to provide more context to the customer when submitting their payment method(s)
  * `total_amount`: The amount in cents the customer will be charged when the free trial period ends
  * `date`: A YYYY-MM-DD string indicating the date the free trial period ends
  * **IMPORTANT**: This field requires `tokenize=true` and `total_amount=0` (free trial subscriptions only)

### Idempotency

`idempotency_key` lets you safely retry a request (e.g. after a network blip, or to dedupe between concurrent retries) without creating a duplicate payment session.

* **Identical retry**: If you call this endpoint again with the same `idempotency_key` and the same key request fields, you'll get back the same payment session `id` (HTTP `200`). The session is **not** re-created.
* **Reused key with a different request**: If you reuse the `idempotency_key` but change one of the key fields, you'll get a `400 IdempotencyViolation`. The `message` will name the specific field(s) that diverged so you can debug.

**Fields compared** (must match the original request to be treated as a retry):

* `customer_email` — compared case-insensitively (`User@example.com` matches `user@example.com`)
* `total_amount`
* `success_url`, `failure_url`
* `order_items` (name, sku, quantity, price, `item_id`, and item-level amount details)
* `kind`
* `payment_token` (when present on the retry)
* `display_next_bill_info.total_amount`, `display_next_bill_info.date`, and the implied `is_free_trial` flag (when present on the original)

**Fields not compared** — differences in these fields do not affect idempotency matching:

* `customer_name`
* `metadata`
* `customer_state`
* `survey_config`
* `billing_name`, `billing_address`, `shipping_name`, `shipping_address`
* `request_multicapture`, `test_config`, and other request configuration

If a retry sends different values for any of these, the original payment session is still returned — the new values are not applied to the existing session.

**How `order_items` is compared.** Line order does not matter, and neither does the order of the entries inside a line's `amount_details`. A cart carrying the same line twice must replay with it twice. `item_id` is part of a line's identity: omit it and Truemed provisions one, so a retry that adds your own id is rejected. Send the same `item_id` on both calls, or omit it on both.

The `400` carries no payment session `id`. If you lost the original response, find the session with [list payment sessions](/api-reference/payment-sessions/list-payment-session).

### iFrame Payment Session

If `use_iframe` is set to `true`, `redirect_url` will be iframe-compatible. When the customer
finishes, the iframe posts one completion message to your page:

```json
{ "success": true, "redirectUrl": "https://example.com/success" }
```

`redirectUrl` echoes the `success_url` or `failure_url` from your request, matching `success`. See
the [Embedded Checkout guide](/guides/setup/api/payment-sessions/i-frame-payment-session) for how to
render the flow, validate the message's origin, and handle it.

## Response Notes

### Success Case

HTTP Status: `200`

* `id` - the unique ID for this checkout session. The same `id` will be returned if the endpoint is called again with the same `idempotency_key` **and** the request's key fields match the original (see [Idempotency](#idempotency) below).
  * Associate this id with your internal representation of a checkout session, if applicable
* `redirect_url` - the URL that the partner should redirect the customer to
  * Typically this would be done after the user presses a "Checkout with Truemed" button or similar on the partner's site
  * The user will perform the following steps on Truemed's platform
    * Complete a health survey to determine eligibility to spend HSA/FSA funds on the purchase
    * Enter HSA/FSA card details, and confirm the purchase
  * The status of the checkout session will be communicated back to the partner via the `payment_session_complete` webhook

### Bad Request

HTTP Status: `400`

If a field is missing or the request is otherwise can't be processed, we'll return a `400` with the following fields:

* `error`: Can be one of:
  * `MissingRequiredParameter`: A required field is not present in the request
  * `InvalidRequestParameter`: A field value is invalid (e.g., malformed email, invalid URL)
  * `TotalAmountInvalid`: The `total_amount` field is either:
    * Not greater than or equal to the sum of the `object_item` `amount` fields
    * The `total_amount` is 0
    * The `total_amount` is negative
  * `OrderItemsInvalid`: The `order_items` array failed validation
  * `IdempotencyViolation`: An `idempotency_key` was reused with different request parameters. The `message` names the specific field(s) that diverged from the original request. See [Idempotency](#idempotency) below for which fields are compared.
  * `ServerError`: There was an internal server error.
    * Requests can be retried as long as the `idempotency-key` is the same.
  * `SessionPending`: A charge to the existing payment method is unable to be processed because the user has yet to enter a payment method.
    * This is only possible when creating a charge using a `payment_token` before the user has finished the initial checkout (for the `payment_session` created with `tokenize=True`).
  * `TokenizeNotSupported`: Both `tokenize=True` and `payment_token` were set. You can't tokenize a payment session if the session is being billed back to a prior one using a `payment_token`.
  * `FeatureNotEnabled`: A required merchant feature is not enabled - for example Stripe configuration, or `request_multicapture=true` on a sales channel without multi-capture enabled or in combination with `tokenize` or `payment_token`
* `message`: A human-readable description of the error

### Unprocessable Entity

HTTP Status: `422`

If a `payment_token` was provided and the `token` requires customer intervention before it can be used again, a `422` will be returned.

* `next_action`: The action the customer must take. Can be one of the following:
  * `UpdatePaymentMethod`: Customer must enter a new payment method.
  * `TakeHealthSurvey`: Customer must retake the health survey
* `redirect_url`: Contains a URI through which the customer can correct the `payment_token`.

Reference: https://developers.truemed.com/api-reference/payment-sessions/create-payment-session

## Authentication

- `x-truemed-api-key` header (required) — Sales channel API key for merchant server-to-server authentication

## Servers

- `https://dev-api.truemed.com` (Development server, default)
- `https://api.truemed.com` (Production server)

## Request

### Body (application/json)

This endpoint expects a CreatePaymentSessionRequest.

- `amount_details` (PaymentSessionAmountDetails, required) — Additional amount details including discounts and charges.
- `customer_email` (string, required) — The email address for the customer.
- `customer_name` (string, required) — The first and last name of the customer.
- `failure_url` (string, required) — When the payment fails in an irrecoverable way, or the user is ineligible to spend HSA/FSA funds on the purchase.
- `idempotency_key` (string, required) — A unique identifier for the checkout session, used to safely retry this request. If the same `idempotency_key` is sent again with a request whose key fields match the original (`customer_email` (case-insensitive), `total_amount`, `success_url`, `failure_url`, `order_items`, `kind`, `payment_token`, and any `display_next_bill_info` fields), the original payment session is returned. If the key is reused with a different request, a `400 IdempotencyViolation` is returned naming the field(s) that diverged. `customer_name`, `metadata`, and survey configuration are not part of the idempotency comparison.
- `order_items` (list of PaymentSessionOrderItem, required) — A list of order items.
- `success_url` (string, required) — Where to redirect the user to after Truemed successfully processes a payment.
- `total_amount` (integer, required) — The total amount in cents to charge the user, inclusive of discounts, shipping, taxes, and other fees being collected by the partner.
- `billing_address` (Address, optional, nullable) — The Billing address for the order (used for better fraud detection)
- `billing_name` (string, optional, nullable) — The first and last name of the person for billing (used for better fraud detection).
- `customer_state` (string, optional, nullable) — The 2-letter ISO 3166-2:US code for a region, without the US- prefix.
- `display_next_bill_info` (NextBillInfo, optional, nullable) — Information about the next bill for free trial sessions.
- `kind` (enum, optional, nullable, default: one_time_payment) — One of the following: one_time_payment (DEFAULT), authorization.
  - Allowed values: `one_time_payment`, `authorization`
- `metadata` (string, optional, nullable) — Will be echoed back in the webhook and detail view if present.
- `payment_token` (string, optional, nullable) — The ID returned by Truemed on initial recurring charges (when tokenize=True).
- `request_multicapture` (boolean, optional, nullable, default: false) — Opts this authorization into multiple partial captures. Requires multi-capture to be enabled on your sales channel, and cannot be combined with `tokenize` or `payment_token` — otherwise the request fails with `FeatureNotEnabled`. Requesting it does not guarantee it: the card network decides, so a later capture with `final: false` can still be rejected. Only meaningful with `kind=authorization`. See [Manual and delayed capture](/guides/setup/api/payment-sessions/manual-delayed-capture#capturing-in-multiple-parts).
- `shipping_address` (Address, optional, nullable) — The shipping address for the payment session.
- `shipping_name` (string, optional, nullable) — The first and last name of the person for shipping (used for better fraud detection).
- `survey_config` (SurveyConfig, optional, nullable) — Allows the partner to customize the health survey.
- `tokenize` (boolean, optional, nullable, default: false) — True or False - set to True to enable subsequent charges.
- `use_iframe` (boolean, optional, nullable, default: false) — Optional query parameter that can be used to modify the response's redirect_url so it can be used and communicated with in an iframe.

## Response

### 200

Successful response

- `id` (string, required) — The unique ID for this checkout session.
- `authorization_expires_at` (datetime, optional, nullable) — The latest UTC time at which the authorization is valid for capture. Null until the session has an authorized transaction. When the transaction has multiple active payments (e.g. HSA + credit), this is the earliest expiration across them.
- `lmn_expires_at` (date, optional, nullable) — When the Letter of Medical Necessity covering this customer expires (inclusive, UTC). Null for sessions that are not tokenized, and for a customer who holds no letter on this sales channel. While a letter is live this is the earliest expiration among them; once none is, it is when the last one lapsed. This is the same value the customer-eligibility endpoints return, so renewal can be driven off whichever call you already make.
- `message` (string, optional, nullable) — Optional message for the response.
- `next_action` (string, optional, nullable) — The next action required for the payment.
- `payment_token` (string, optional, nullable) — The payment token for recurring payments.
- `redirect_url` (string, optional, nullable) — The URL that the partner should redirect the customer to.

## Errors

### 400 Bad Request Error

Bad Request

- `error` (enum, required)
  - Allowed values: `FeatureNotEnabled`, `IdempotencyViolation`, `InvalidRequestParameter`, `MissingRequiredParameter`, `OrderItemsInvalid`, `SessionPending`, `TokenizeNotSupported`, `TotalAmountInvalid`, `ValidationError`
- `message` (string, required)

### 404 Not Found Error

Not Found

- `error` (enum, required)
  - Allowed values: `NotFound`
- `message` (string, required)

### 405 Method Not Allowed Error

Method Not Allowed

- `error` (enum, required)
  - Allowed values: `MethodNotAllowed`
- `message` (string, required)

### 500 Internal Server Error

Internal Server Error

- `error` (enum, required)
  - Allowed values: `ServerError`
- `message` (string, required)

### 501 Not Implemented Error

Not Implemented

- `error` (enum, required)
  - Allowed values: `NotImplemented`
- `message` (string, required)

## Types

### PaymentSessionAmountDetails

- `additional_charges_details` (list of PaymentSessionAmountDetail, optional, nullable) — An array of individual additional charge line items with names and amounts.
- `discounts_details` (list of PaymentSessionAmountDetail, optional, nullable) — An array of individual discount line items with names and amounts.
- `total_additional_charges` (integer, optional, nullable) — The total amount of additional charges (e.g., shipping, tax), in cents.
- `total_discounts` (integer, optional, nullable) — The total amount of discounts applied, in cents.

### PaymentSessionOrderItem

- `amount_details` (PaymentSessionAmountDetails, required) — Optional itemized discounts and charges for this item.
- `name` (string, required) — The name of the item.
- `quantity` (integer, required) — The number of this item in the cart.
- `sku` (string, required) — The SKU (Stock Keeping Unit) for this item.
- `amount` (integer, optional, nullable) — The total amount for this line item in cents. Either amount or price must be provided.
- `item_id` (string, optional, nullable) — A unique identifier for this order item within the payment.
- `price` (integer, optional, nullable) — The unit price in cents for the item. Either amount or price must be provided.

### Address

- `city` (string, required) — City for address
- `line1` (string, required) — First line of address
- `postal_code` (string, required) — Zip code for address
- `state` (string, required) — State for address
- `country` (string, optional, nullable) — 2-letter ISO 3166 for address
- `line2` (string, optional, nullable) — Second line of address

### NextBillInfo

- `date` (date, required)
- `total_amount` (integer, required)

### SurveyConfig

- `hide_back_to_checkout_cta` (boolean, required) — Provide as true to hide the 'Back to checkout' CTA in the survey if the user fails to qualify.

### PaymentSessionAmountDetail

- `amount` (integer, required) — The amount in cents for this discount or charge.
- `name` (string, required) — The name or description of the discount or charge.

## Examples

**Request**

```json
{
  "customer_email": "string",
  "customer_name": "string",
  "failure_url": "string",
  "idempotency_key": "string",
  "order_items": [
    {
      "amount_details": {},
      "name": "string",
      "quantity": 1,
      "sku": "string"
    }
  ],
  "success_url": "string",
  "total_amount": 1
}
```

**Response**

```json
{
  "id": "string",
  "authorization_expires_at": "2024-01-15T09:30:00Z",
  "lmn_expires_at": "2023-01-15",
  "message": "string",
  "next_action": "string",
  "payment_token": "string",
  "redirect_url": "string"
}
```

**SDK Code**

```python
import requests

url = "https://dev-api.truemed.com/payments/v1/create_payment_session"

payload = {
    "customer_email": "string",
    "customer_name": "string",
    "failure_url": "string",
    "idempotency_key": "string",
    "order_items": [
        {
            "amount_details": {},
            "name": "string",
            "quantity": 1,
            "sku": "string"
        }
    ],
    "success_url": "string",
    "total_amount": 1
}
headers = {
    "x-truemed-api-key": "<apiKey>",
    "Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers)

print(response.json())
```

```javascript
const url = 'https://dev-api.truemed.com/payments/v1/create_payment_session';
const options = {
  method: 'POST',
  headers: {'x-truemed-api-key': '<apiKey>', 'Content-Type': 'application/json'},
  body: '{"customer_email":"string","customer_name":"string","failure_url":"string","idempotency_key":"string","order_items":[{"amount_details":{},"name":"string","quantity":1,"sku":"string"}],"success_url":"string","total_amount":1}'
};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```