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

# Embedded checkout (iFrame)

> How to render the Truemed payment session inline with an iFrame instead of redirecting the customer to a hosted page.

The iFrame is an alternative *presentation* of the standard flow—the same eligibility
survey, card entry, statuses, and webhooks as the
[hosted redirect](/guides/setup/api/payment-sessions/accept-your-first-payment), just rendered inline on your
page instead of sending the customer to a Truemed-hosted page. Use it when you want customers to
stay on your site through checkout.

It works for both entry points:

* **One-time payments** — [`create_payment_session`](/api-reference/payment-sessions/create-payment-session), for a single checkout.
* **Subscriptions** — [`create_payment_token`](/api-reference/payment-tokens/create-payment-token), where the customer takes the survey and stores their cards once, and you charge on your own schedule afterwards.

Everything you build for the redirect flow still applies—creating the session or token, handling
the webhook, and fulfilling on it. Only two things change:

|                               | Hosted redirect (default)                         | Embedded iFrame                       |
| ----------------------------- | ------------------------------------------------- | ------------------------------------- |
| Create the session or token   | `create_payment_session` / `create_payment_token` | The same call with `use_iframe: true` |
| Where the customer checks out | A Truemed-hosted page                             | Inline, in an iframe on your page     |
| How you learn the result      | `success_url` / `failure_url` redirect + webhook  | `postMessage` event + webhook         |

> **Note**
>
> Fulfillment is unchanged: the `payment_session_complete` webhook with `status: captured` is still the
> source of truth. The `postMessage` `success` flag is only for updating your own UI.

## Enable the iFrame

Add the optional `use_iframe` parameter to
[`create_payment_session`](/api-reference/payment-sessions/create-payment-session) or
[`create_payment_token`](/api-reference/payment-tokens/create-payment-token). When `use_iframe`
is `true`, the `redirect_url` in the response is an iframe-compatible URL you render inline instead of
redirecting to.

If every checkout on your integration is embedded, ask your Truemed contact to enable it on your sales
channel instead. The returned URL is then always iframe-compatible and you can leave the parameter out
of your requests entirely.

### Subscriptions: the whole signup happens in one frame

On [`create_payment_token`](/api-reference/payment-tokens/create-payment-token) the customer takes the
health survey and then enters their cards. Both steps render **inside the same frame**, one after the
other—you set `src` once and do not swap it between steps or manage the sequence. The message arrives
when the customer has finished storing their cards, and your page never navigates along the way.

### Example request

```json
{
  "use_iframe": true,
  "total_amount": 0,
  "order_items": [],
  "success_url": "https://example.com/success",
  "failure_url": "https://example.com/failure",
  "idempotency_key": "key",
  "customer_email": "customer@example.com",
  "customer_name": "Customer"
}
```

### Example response

```json
{
  "id": "session_id",
  "redirect_url": "https://truemed.com/..."
}
```

## Environments and local development

There is nothing to install and nothing to configure on your side. No SDK, no publishable key in the
browser, and no origin to register with us before you can start — you set an iframe `src` and add one
listener.

Two values differ between environments:

|                                                          | Sandbox                       | Production                |
| -------------------------------------------------------- | ----------------------------- | ------------------------- |
| API base — server-side only                              | `https://dev-api.truemed.com` | `https://api.truemed.com` |
| Truemed origin — what you compare `event.origin` against | `https://dev.truemed.com`     | `https://app.truemed.com` |

You never hardcode the frame's URL; it arrives as `redirect_url` on the create call. The origin is the
only Truemed value that belongs in your frontend config, so one setting per environment covers it.

Sandbox API keys come from the sandbox dashboard at
[dev.truemed.com/developers/api-keys](https://dev.truemed.com/developers/api-keys). Keys are not shared
across environments.

### Running against localhost

A page served from `http://localhost` can embed the sandbox flow with no setup on either side. We send
no `X-Frame-Options` header and no `frame-ancestors` policy on the framed pages, and those pages
authenticate on identifiers already present in the URL rather than on cookies—so third-party-cookie
rules and browser privacy settings have no bearing on the embed. A local page behaves the same as your
production page.

> **Note**
>
> We do not post height or resize messages. The completion message is the only one you receive from us,
> so give the frame generous height and let it scroll internally rather than trying to size it to its
> contents.

### If a create call returns 404

A `404` with `{"error": "Page not found"}` means the API key is wrong, or the sales channel is not
enabled for the endpoint you are calling. We return `404` rather than `401` so that an unauthenticated
caller cannot map which endpoints exist, which makes a credentials problem read like a bad path. If the
path is right, check the key and the environment before you check the URL.

## Handling iFrame responses

Set the iframe element's `src` to `redirect_url` and Truemed's survey and checkout render inside it.
The embedded page never navigates your page and cannot remove its own iframe—when the customer
finishes, Truemed posts a single completion message to your window via the browser's
[postMessage API](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage). That message
is your signal to remove the iframe and update your page.

### The completion message

The completion message is the only message Truemed sends. Its `data` is a plain object:

**`Customer completed checkout`**

```json title="Customer completed checkout"
{ "success": true, "redirectUrl": "https://example.com/success" }
```

**`Customer exited or was ineligible`**

```json title="Customer exited or was ineligible"
{ "success": false, "redirectUrl": "https://example.com/failure" }
```

| Field         | Type    | Description                                                                                                                                                                                                                                                                                                         |
| ------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `success`     | boolean | `true` when the customer completed the flow, `false` when they exited or were ineligible. For your UI only—fulfill from the `payment_session_complete` webhook.                                                                                                                                                     |
| `redirectUrl` | string  | An echo of the `success_url` or `failure_url` you provided on the create call, matching `success`. In the hosted flow Truemed redirects the browser to these URLs; inside an iframe it can't, so it hands you the URL instead. Navigate to it for hosted-flow behavior, or ignore it and update your page in place. |

> **Warning**
>
> Check `event.origin` before you act on a message. Your page receives messages from anything it frames,
> including the payment provider during a 3D Secure challenge, and any page on the internet can frame
> yours and post a message that looks exactly like ours. Comparing the origin against the Truemed origin
> is the only check that tells our message apart from an imitation of it. The origin for each
> environment is in the table above, and it differs between sandbox and production—read it from your
> environment config rather than hardcoding one, or the wrong build will discard every message we send.
>
> Pair it with `event.source !== iframe.contentWindow` if your page can open checkout more than once,
> so a completion message is only ever handled by the frame that is currently open.

Apple Pay requires `allow="payment"` on the iframe and HTTPS on the parent page. Card payments and Link work without it.

### 3D Secure

If the customer's bank asks them to authenticate, the 3D Secure challenge renders inside the embedded
flow and the customer completes it there. There is no redirect, no popup, and nothing extra to build.

Give the frame enough height for it—the challenge is the bank's own screen, and a short frame makes it
awkward to use. Note that your `message` listener will also receive events from the authentication
provider while the challenge is open, which is one reason to be deliberate about which messages you act
on (see below).

### Example handler

```javascript
// The one Truemed value that belongs in your frontend config. Read it per environment —
// 'https://dev.truemed.com' in sandbox, 'https://app.truemed.com' in production — rather than
// hardcoding a literal, or a sandbox build will silently discard every message production sends
// and vice versa.
const TRUEMED_ORIGIN = window.APP_CONFIG.truemedOrigin;

function openTruemedIframe(redirectUrl) {
  const iframe = document.createElement('iframe');
  iframe.id = 'truemed-checkout';
  iframe.src = redirectUrl;
  iframe.allow = 'payment';
  iframe.style.width = '100%';
  iframe.style.height = '100%';

  document.body.appendChild(iframe); // Or append to any desired element

  function handleMessage(event) {
    if (event.origin !== TRUEMED_ORIGIN || event.source !== iframe.contentWindow) {
      // Anything else on the page — including the 3D Secure challenge — lands here. Ignore it.
      return;
    }

    const { success, redirectUrl } = event.data ?? {};
    if (success === undefined || redirectUrl === undefined) {
      // Handle no data or invalid data shape
      // We typically recommend ignoring these, defaulting to no-op unless you explicitly recognize the event.
      return;
    }

    // Tear down before you act on the result, so that reopening checkout after a failure
    // does not leave an earlier listener bound to a frame that is already gone.
    window.removeEventListener('message', handleMessage);
    document.body.removeChild(iframe);

    if (success) {
      // Handle success
      // ...
      // or redirect to previously provided success_url
      window.location.href = redirectUrl;  // Redirect to success URL
    } else {
      // Handle failure
      // ...
      // or redirect to previously provided failure_url
      window.location.href = redirectUrl;  // Redirect to failure URL
    }
  }

  window.addEventListener('message', handleMessage);
}
```