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

# Embed a presentation preview

> Create a scoped, expiring Presenton URL and display a presentation securely in your application with an iframe.

Use the Cloud API to create a temporary presentation URL, then load that URL in an iframe. This lets people view, edit, or export a specific presentation inside your application without exposing your Presenton API key or account session in browser code.

<Note>
  The integration endpoint is available in the Presenton Cloud API v3. Create the iframe URL on your server, not directly in the browser.
</Note>

## How the integration works

1. Your browser asks your application server for a presentation preview.
2. Your server verifies that the signed-in application user may access the presentation.
3. Your server calls `POST /api/v3/presentation/integrate` with its Presenton API key.
4. Presenton returns a scoped, expiring `frontend_url` containing a presentation token.
5. Your server returns that URL to the browser, which uses it as the iframe `src`.

The presentation token can access only the selected presentation and only the capabilities listed in `scopes`. It does not expose your main API key.

## Before you begin

You need:

* A Presenton Cloud API key stored in a server-side secret such as `PRESENTON_API_KEY`.
* The UUID of a presentation owned by the same Presenton account.
* A backend route in your application that authenticates your users before creating preview URLs.

If you generate the presentation through the API, use the returned `presentation_id`. You can also retrieve presentation IDs with [List presentations](/api-reference/v3-presentation/list-presentations).

## 1. Choose the iframe permissions

Every request must include `presentation:read`. Add only the capabilities your embedded experience needs.

| Scope                 | Embedded capability                                                   |
| --------------------- | --------------------------------------------------------------------- |
| `presentation:read`   | Load and view the presentation. Required for every integration token. |
| `presentation:edit`   | Use edit-capable actions, including chat edits and slide updates.     |
| `presentation:export` | Export the embedded presentation.                                     |

For a preview, use only:

```json theme={null}
{
  "scopes": ["presentation:read"]
}
```

For an embedded editor that can also export, request all three scopes:

```json theme={null}
{
  "scopes": [
    "presentation:read",
    "presentation:edit",
    "presentation:export"
  ]
}
```

<Warning>
  Anyone who obtains the generated URL can use its granted capabilities until it expires. Keep the scope set narrow and the lifetime short.
</Warning>

## 2. Create an integration URL

Call the integrate endpoint from your server with your Presenton API key:

```bash theme={null}
curl --request POST \
  --url https://api.presenton.ai/api/v3/presentation/integrate \
  --header "Authorization: Bearer $PRESENTON_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "presentation": "00000000-0000-0000-0000-000000000000",
    "scopes": ["presentation:read"]
  }'
```

### Request fields

| Field          | Required | Description                                                                                                           |
| -------------- | -------- | --------------------------------------------------------------------------------------------------------------------- |
| `presentation` | Yes      | UUID of the presentation to embed. The authenticated Presenton account must own it.                                   |
| `scopes`       | Yes      | One or more allowed capabilities. The list must include `presentation:read`.                                          |
| `expires_at`   | No       | Future ISO 8601 date and time. Defaults to 24 hours after creation and cannot be more than three days after creation. |

To set a shorter lifetime, calculate a future UTC timestamp when your server makes the request and send it as `expires_at`. The server example in the next section creates a one-hour link.

```json theme={null}
{
  "presentation": "00000000-0000-0000-0000-000000000000",
  "scopes": ["presentation:read"],
  "expires_at": "<future ISO 8601 timestamp>"
}
```

Presenton returns the token metadata and a ready-to-use URL:

```json theme={null}
{
  "token": "<presentation-token>",
  "frontend_url": "https://presenton.ai/presentation?id=00000000-0000-0000-0000-000000000000&type=smart&token=<presentation-token>",
  "user": "11111111-1111-1111-1111-111111111111",
  "presentation": "00000000-0000-0000-0000-000000000000",
  "scopes": ["presentation:read"],
  "version": "smart",
  "expires_at": "<ISO 8601 expiry>"
}
```

Use `frontend_url` exactly as returned. It already includes the presentation ID, presentation type, and scoped token; you do not need to construct the URL yourself.

## 3. Add a server endpoint

The following Express route creates a one-hour, read-only preview URL. Replace `requireUser` and `userCanViewPresentation` with your application's authentication and authorization checks.

```javascript theme={null}
import express from "express";

const app = express();
app.use(express.json());

app.post(
  "/api/presentations/:presentationId/preview",
  requireUser,
  async (request, response) => {
    const { presentationId } = request.params;

    if (!(await userCanViewPresentation(request.user, presentationId))) {
      return response.sendStatus(403);
    }

    const expiresAt = new Date(Date.now() + 60 * 60 * 1000).toISOString();
    const presentonResponse = await fetch(
      "https://api.presenton.ai/api/v3/presentation/integrate",
      {
        method: "POST",
        headers: {
          Authorization: `Bearer ${process.env.PRESENTON_API_KEY}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          presentation: presentationId,
          scopes: ["presentation:read"],
          expires_at: expiresAt,
        }),
      },
    );

    if (!presentonResponse.ok) {
      return response.status(502).json({
        error: "Could not create the presentation preview",
      });
    }

    const integration = await presentonResponse.json();
    response.set("Cache-Control", "no-store");
    return response.json({
      url: integration.frontend_url,
      expiresAt: integration.expires_at,
    });
  },
);
```

Do not return your Presenton API key or place it in a public environment variable. The browser needs only the temporary `frontend_url`.

## 4. Render the iframe

Use the URL returned by your backend as the iframe source.

<Tabs>
  <Tab title="HTML and JavaScript">
    ```html theme={null}
    <iframe
      id="presenton-preview"
      class="presentation-frame"
      title="Presentation preview"
      allowfullscreen
    ></iframe>

    <script>
      async function loadPresentationPreview(presentationId) {
        const response = await fetch(
          `/api/presentations/${presentationId}/preview`,
          { method: "POST" },
        );

        if (!response.ok) {
          throw new Error("Could not load the presentation preview");
        }

        const { url } = await response.json();
        document.querySelector("#presenton-preview").src = url;
      }

      loadPresentationPreview("00000000-0000-0000-0000-000000000000");
    </script>
    ```
  </Tab>

  <Tab title="React">
    ```jsx theme={null}
    import { useEffect, useState } from "react";

    export function PresentationPreview({ presentationId }) {
      const [previewUrl, setPreviewUrl] = useState(null);
      const [error, setError] = useState(null);

      useEffect(() => {
        const controller = new AbortController();

        async function loadPreview() {
          try {
            const response = await fetch(
              `/api/presentations/${presentationId}/preview`,
              { method: "POST", signal: controller.signal },
            );

            if (!response.ok) throw new Error("Preview request failed");
            const { url } = await response.json();
            setPreviewUrl(url);
          } catch (previewError) {
            if (previewError.name !== "AbortError") setError(previewError);
          }
        }

        loadPreview();
        return () => controller.abort();
      }, [presentationId]);

      if (error) return <p>Could not load the presentation.</p>;
      if (!previewUrl) return <p>Loading presentation…</p>;

      return (
        <iframe
          src={previewUrl}
          title="Presentation preview"
          className="presentation-frame"
          allowFullScreen
        />
      );
    }
    ```
  </Tab>
</Tabs>

Add responsive sizing in your application stylesheet:

```css theme={null}
.presentation-frame {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  min-height: 420px;
  border: 0;
  border-radius: 12px;
}

@media (max-width: 640px) {
  .presentation-frame {
    min-height: 70vh;
  }
}
```

Always provide a descriptive `title` for assistive technology. If the presentation is essential content, also provide a visible fallback link or explanation outside the iframe.

## Refresh expired previews

An expired token cannot be extended. Request a new integration URL from your backend and replace the iframe `src`.

A practical pattern is to keep the returned `expiresAt` value in memory and request a replacement shortly before it expires. Do not persist integration URLs in local storage, analytics events, logs, or shared caches.

## Security checklist

* Call the integrate endpoint only from trusted server-side code.
* Authorize the current application user before creating a URL for a presentation.
* Use `presentation:read` alone for preview-only experiences.
* Choose the shortest useful expiry; the maximum is three days.
* Send `Cache-Control: no-store` when your backend returns the URL.
* Treat `frontend_url` as a temporary credential because it contains the token.
* Create a new URL when permissions change instead of reusing an older URL.

## Troubleshooting

| Symptom                                            | Likely cause                                        | Resolution                                                                 |
| -------------------------------------------------- | --------------------------------------------------- | -------------------------------------------------------------------------- |
| `401 Unauthorized` from the integrate endpoint     | Missing or invalid API key                          | Verify the server-side `Authorization: Bearer ...` header.                 |
| `403` when creating the URL                        | The Presenton account does not own the presentation | Use a presentation created by the authenticated account.                   |
| `422 Validation Error`                             | Invalid UUID, scope, or request shape               | Include `presentation:read` and send a valid presentation UUID.            |
| `400` for `expires_at`                             | Expiry is in the past or more than three days away  | Send a future timestamp within the allowed window.                         |
| Preview loads but editing or export is unavailable | The token lacks the corresponding scope             | Create a new URL with `presentation:edit` or `presentation:export`.        |
| Preview stops loading later                        | The integration token expired                       | Request a fresh URL and replace the iframe source.                         |
| Browser refuses to frame the page                  | A self-hosted frontend restricts frame ancestors    | Configure the frontend to allow your application origin, then redeploy it. |

<Card title="Create a presentation integration token" icon="key" href="/api-reference/v3-presentation/create-a-presentation-integration-token">
  Review the complete request and response schema in the API reference.
</Card>
