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

# Control access to the embedded panel

> Decide how the embedded panel identifies the Content Editor for a page and who can see it.

To embed Surfer in your CMS, contact [sales@surferseo.com](mailto:sales@surferseo.com).

The panel shows one Content Editor, the document Surfer builds around your target keywords, holding the guidelines and the Content Score ([The guidelines panel](/integrations/guidelines-panel)). It finds that Content Editor through a permalink, and it identifies the viewer either through their own Surfer session or not at all.

The browser requirements are under [Availability](/integrations/embed/quickstart#availability).

## Two ways to identify the writer

Leave `anonymous` unset and the panel uses the writer's own Surfer session. Set `anonymous: true` and the panel shows one Content Editor to whoever opens the page, with no Surfer sign-in.

In signed-in mode the panel signs in with the Surfer session in the writer's browser. That session reaches the panel through third-party cookies or storage access ([Sign in and cookies](/integrations/guidelines-panel#sign-in-and-cookies)). When the permalink resolves to no Content Editor, the writer links or creates one from the dashboard ([When nothing matches](#when-nothing-matches)).

In shared mode you pass `anonymous: true` and a permalink that already resolves, usually one read from the API ([Connect to an existing Content Editor](#connect-to-an-existing-content-editor)). The panel never reads the viewer's Surfer session, even when the browser has one.

Whoever opens the page sees that Content Editor's guidelines and score. Nothing in the panel lets them pick another Content Editor, create one, disconnect it, or open its settings.

The two modes side by side:

| Aspect | Signed-in mode | Shared mode |
| - | - | - |
| `anonymous` | Unset or `false` | `true` |
| Who the panel identifies | The writer, through their Surfer session | Nobody; any Surfer session in the browser is ignored |
| Permalink | The page URL that `init` derives, or any value you pass | A value that already resolves, usually a `hash` from the API |
| Page with no Content Editor | The dashboard, to link or create one ([What you see](/integrations/embed/quickstart#what-you-see)) | The connect screen, from which the panel cannot proceed |
| **Disconnect Content Editor from this page** and **Customize** | Shown to writers with rights to the Content Editor | Never shown |
| Fits | Your own writers, each signed in to Surfer | Your users, who have no Surfer account |

## Permalinks

A permalink is the string that ties a page in your CMS to one Content Editor. Surfer resolves it by its value alone.

Every Content Editor has two permalinks, `edit` and `comment`. Either one opens the panel. Use a nonempty, stable string as the permalink: the page URL, a token-prefixed URL, a `hash` from the API, or a random identifier of your own.

How the library sets one:

* `init` uses the page's own URL, without its fragment, when you give no `permalink` option.
* `init` uses the `permalink` option when you give one.
* `init` prefixes a sharing token, when given, to either value ([Sharing tokens](#sharing-tokens)).
* `initWithOptions` returns a handle whose `setPermalink` sends the value as you built it; the library adds nothing to it.

Surfer looks the value up as it arrives, with no account check. Anyone who has a permalink can open that Content Editor's guidelines in a panel, signed in or not, so treat a permalink as a secret. The guidelines also need an active subscription on the organization that owns the Content Editor ([Plans](/integrations/guidelines-panel#plans)).

> [!WARNING]
>
> An `edit` permalink also opens the document for editing in the Surfer app,
> without a sign-in. A `comment` one opens it for viewing and commenting. A page
> URL that anyone can guess is a permalink that anyone can guess; prefix a
> [sharing token](#sharing-tokens) before you rely on it.

## Connect to an existing Content Editor

Read the Content Editor's permalinks from the API and pass the `hash` as the permalink.

This workflow needs API access, an API key, and the workspace and Content Editor IDs. [Authentication](/api/authentication) covers who has access and how to get a key.

**1. Pick or create the Content Editor**

Create it through the API ([Create Content
Editor](/api/v2/content-editors/create-content-editor)) or pick an existing
one.

**2. Wait for the Content Editor to complete**

The permalink endpoints answer `409` until its `state` is `completed`.

**3. List its permalinks**

Call [List Content Editor
permalinks](/api/v2/permalinks/list-content-editor-permalinks). Each item
carries three fields:

* `type` is `edit` or `comment`.
* `hash` is the value the panel resolves.
* `url` is the address of that permalink in the Surfer app, for people.

**4. Pass the hash to the panel**

A sharing token would be prefixed to the hash, and the result would resolve
to nothing. Pass the `hash` as the `permalink` option of `init`, or to
`setPermalink`, with no sharing token.

The panel opens with that Content Editor's guidelines for whoever loads the page, in either mode.

A reset gives the Content Editor new permalinks, and the old ones stop working ([Revoke share links](/api/use-cases#revoke-share-links)). A page still passing an old value gets the dashboard or the connect screen until you pass the new one. Update the page in the same change.

v1 returns the `edit` permalink as `permalink_hash` on [Get Content Editor query](/api/v1/content-editors/get-content-editor-query), with no `url`. It is the same value as the `edit` hash in v2.

## Sharing tokens

When your page URLs are guessable, prefix a secret token to the permalink, unique per document and never changed.

`init` takes the token as its second argument, joins it to the permalink with no separator, and URL-encodes the result. That encoded string is the permalink Surfer stores and resolves. `init` builds it the same way from the page URL it derives and from a `permalink` option.

Add this to your editor page after the loader snippet from the [quickstart](/integrations/embed/quickstart), where `token` holds the token stored with the document:

```js theme={"system"}
const container = document.getElementById('surfer-guidelines')
window.surferGuidelines.init(container, token)
```

For a page at `https://your.cms.com/posts/42` and the token `7f0c2d1e`, the permalink Surfer stores is the URL-encoded form of:

```text theme={"system"}
7f0c2d1ehttps://your.cms.com/posts/42
```

Three rules keep the token doing its job:

* Generate it once per document, random and long enough not to be guessed. A UUID works. One token for the whole integration lets anyone who holds one permalink guess the others.
* Store it with the document and pass the same value on every load.
* Never change it. A changed token makes a different permalink, which resolves to nothing. The page then shows the dashboard or the connect screen, and the Content Editor stays linked to the old value until someone links it again.

`initWithOptions` has no token argument. Pass `encodeURIComponent(token + url)` to `setPermalink` ([JavaScript API](/integrations/embed/javascript-api)).

## When nothing matches

A permalink that resolves to no Content Editor gets the dashboard for a signed-in writer and the connect screen for everyone else. The quickstart's table under [What you see](/integrations/embed/quickstart#what-you-see) shows what each state looks like. Creating a Content Editor from the dashboard spends one Content Editor credit ([Credits](/integrations/guidelines-panel#credits)).

What follows for your page:

* Linking a Content Editor from the dashboard sets its `edit` permalink to the page's permalink, so the API lists that value from then on.
* Linking replaces any page the Content Editor was linked to before.
* In shared mode the connect screen leads nowhere, because the panel ignores any Surfer session. Pass a permalink that resolves.

The library reports the outcome to your page through `onDraftLoaded`, with `null` when nothing matched ([JavaScript API](/integrations/embed/javascript-api)).

## White-label organizations

The permalink URLs the API returns use your organization's white-label domain when one is set. The panel itself always loads from `https://app.surferseo.com`, whatever domain the organization uses, and is not white-labeled; that is Surfer's policy for the embed.

The `hash` is the same on a white-label organization, and the panel resolves it the same way. Which domain the `url` is built on is under [Use cases](/api/use-cases#create-content-editors-from-a-keyword-list-and-share-them-with-writers).

## Not offered

The embed shows and scores; it does not edit, write, or rebrand. The panel is never white-labeled ([White-label organizations](#white-label-organizations)), and [What stays in the Surfer app](/integrations/guidelines-panel#what-stays-in-the-surfer-app) lists everything else the panel leaves to Surfer.


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