> ## Documentation Index
> Fetch the complete documentation index at: https://photonhealth.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# iframe

> Add Prescribe to any frontend with a URL.

The iframe is the full prescribing flow in a URL. It works with any frontend framework and signs the prescriber in to Photon with your Application `client_id`.

Build the URL with `frame()` from the [JavaScript client](/docs/prescribe/embed/javascript), created with your Application `client_id`. `frame()` puts the `client_id` in the URL, and the iframe opens only from a URL that has one.

```ts theme={"dark"}
import { photon } from "@photon-rx/core";

const ph = photon({ environment: "neutron", auth: { clientId: "YOUR_CLIENT_ID" } });

const src = ph.order.frame({ patient: { id: "pat_01M3WQQHRB1T0K1469KD18BAWX" } });
// https://rx.neutron.health/order/embed?patient=pat_01M3WQQHRB1T0K1469KD18BAWX&client=YOUR_CLIENT_ID
```

Use it as your iframe's `src`, as in [Height and events](#height-and-events). The page has no chrome and a transparent background, so your page shows through.

<Card title="Try it" icon="play" href="https://rx.neutron.health/integrate/order/embed" horizontal arrow>
  The iframe in /integrate, with the URL `frame()` builds, the host code and every message it posts.
</Card>

## What you need

* An **Application** `client_id` from the [Neutron dashboard](https://app.neutron.health/settings/developers). Embedding needs API credentials, which come with [Launch and above](/docs/use-cases#plans-at-a-glance).
* In that credential's allowed URLs:
  * Every domain where your pages show the iframe.
  * The Prescribe app's domain, where the iframe signs the prescriber in: `https://rx.neutron.health` in the sandbox, or `https://rx.new` in production.

## Building the URL

Each record's `frame()` takes Photon ids only, and reads and writes nothing. The URL uses the same paths and parameters as [deep links](/docs/prescribe/app/deep-links), under `/embed`.

| Call | Opens |
| - | - |
| `ph.order.frame()` | A new order. The prescriber finds or adds the patient. |
| `ph.order.frame({ patient: { id: "pat_…" } })` | A new order for this patient |
| `ph.order.frame({ patient: { id: "pat_…" }, prescriptions: { add: [{ id: "rx_…" }] } })` | A new order for this patient, with this draft on it |
| `ph.order.frame({ patient: { id: "pat_…" }, prescriptions: { add: [{ templateId: "tmp_…" }] } })` | A new order for this patient, with a prescription from this template |
| `ph.order.frame("ord_…")` | This order |
| `ph.patient.frame("pat_…")` | This patient |
| `ph.prescription.frame("rx_…")` | This prescription |

URLs start with `https://rx.neutron.health` in the sandbox, or `https://rx.new` in production, which is in private preview.

To give a flow more than one record, pass the whole link to `ph.frame()`:

```ts theme={"dark"}
const src = ph.frame({
  flow: "order",
  patient: { id: "pat_01M3WQQHRB1T0K1469KD18BAWX" },
  order: { prescriptions: { add: [{ templateId: "YOUR_TEMPLATE_ID" }] } },
});
```

A URL carries Photon ids only, never patient details. `frame()` refuses anything else. To start from a patient Photon hasn't matched yet, sync their details from your backend first ([patient sync](/docs/network/patient)) and pass the id it returns.

A live object you already have makes its own: `order.frame()`.

When the URL names the patient by id, the iframe stays on that patient, with no **Change patient**.

A URL without a `client_id` shows **This link can't be embedded** and does nothing else. An id Photon doesn't have in the prescriber's organization opens as **not found** on its card, and the rest still opens.

## Height and events

The iframe posts messages to your page. They contain ids and states only, never patient details.

| `event` | Sent | Other fields |
| - | - | - |
| `resize` | Whenever the content's height changes | `height`, in CSS pixels |
| `PhotonOrder.onPatient` | The order's patient has a record or changed | `detail`: the patient's id, or `null (changed)` when the prescriber changes patient |
| `PhotonOrder.onOrder` | The order or one of its prescriptions changed | `detail`: a summary that starts with the order's id |
| `PhotonOrder.onSent` | The order was sent | `detail`: the order's id and state, such as `ord_… · submitted` |
| `PhotonPatient.onPatient` | The patient has a record or changed | `detail`: the patient's id, or `null (changed)` when the prescriber changes patient |
| `PhotonPrescription.onPrescription` | The prescription was saved or signed | `detail`: a summary that starts with its id |
| `PhotonPrescription.onSigned` | The prescription was signed | `detail`: its id |

`detail` is a summary for logs. Read the id at its start, and don't parse the rest: its wording can change.

Every message has `source: "photon-rx"`. Use the `resize` message to size the iframe, because the iframe has no fixed height:

<CodeGroup>
  ```tsx React theme={"dark"}
  import { useEffect, useState } from "react";

  export function PrescribeFrame({ src, onSent }) {
    const [height, setHeight] = useState(0);
    useEffect(() => {
      const onMessage = ({ origin, data }) => {
        if (origin !== "https://rx.neutron.health" || data?.source !== "photon-rx") return;
        if (data.event === "resize") setHeight(data.height);
        if (data.event === "PhotonOrder.onSent") onSent();
      };
      window.addEventListener("message", onMessage);
      return () => window.removeEventListener("message", onMessage);
    }, [onSent]);
    return <iframe src={src} title="Prescribe" style={{ width: "100%", height, border: 0 }} />;
  }
  ```

  ```html HTML theme={"dark"}
  <iframe id="photon" title="Prescribe" style="width: 100%; border: 0"></iframe>
  <script type="module">
    import { photon } from "@photon-rx/core";

    const ph = photon({ environment: "neutron", auth: { clientId: "YOUR_CLIENT_ID" } });
    const frame = document.getElementById("photon");
    frame.src = ph.order.frame({ patient: { id: "pat_01M3WQQHRB1T0K1469KD18BAWX" } });

    window.addEventListener("message", ({ origin, data }) => {
      if (origin !== "https://rx.neutron.health" || data?.source !== "photon-rx") return;
      if (data.event === "resize") frame.style.height = `${data.height}px`;
      if (data.event === "PhotonOrder.onSent") closeDialog();
    });
  </script>
  ```
</CodeGroup>

## Sign-in

The first time, the iframe shows **Sign in to Photon**. Clicking it opens a popup, where the prescriber signs in with your Application `client_id` and picks their organization. Then the iframe continues, and later loads stay signed in.

To skip the organization picker, give the client your organization, and every URL it makes carries it:

```ts theme={"dark"}
const ph = photon({ environment: "neutron", auth: { clientId: "YOUR_CLIENT_ID" }, organization: "org_…" });
```

The popup needs your page to allow it:

* If your iframe has a `sandbox` attribute, include `allow-popups allow-scripts allow-same-origin`.
* If your page sends a `Cross-Origin-Opener-Policy` header, use `same-origin-allow-popups`, not `same-origin`.

## Only the iframe can be framed

Photon allows only the `/embed` pages in another site's iframe, on the domains in your credential's allowed URLs. The Prescribe app's other pages refuse to load in a frame anywhere else. To send a prescriber to the full app, use a [deep link](/docs/prescribe/app/deep-links).


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