---
title: "Your identity endpoint"
description: "Sign visitors into an agent on your custom domain with your own auth — one credentialed endpoint on your site, one exact CORS contract."
---

If your app uses **bring-your-own auth** and is served on a [custom
domain](/domains/), visitors sign in through an **identity endpoint** you
host: a URL on your own site that recognizes the visitor's existing
session cookie and returns a short-lived Guuey token for them. The agent
page never sees your credentials or your session — it only receives the
token your endpoint chooses to mint.

This is the standalone-page counterpart of the [identified embed's token
endpoint](/embed/#who-the-visitor-is): the minting is identical, only the
transport differs. It is one of two bring-your-own ways to sign visitors
into the page — the other, a redirect to [your own identity
provider](/page-sign-in/), works on every host (Guuey's included) but
costs a full-page hop; this one is silent where your session cookie is
reachable. An embed lives inside your page and can call your
backend directly; a standalone page on `chat.example.com` is its own
origin, so the browser fetches your endpoint **cross-origin with
credentials** — which is why the CORS contract below is exact.

## How it works

1. A visitor opens your agent on your custom domain, already signed in to
   your site in the same browser.
2. The page requests your identity endpoint — a plain `GET` with
   `credentials: "include"` and `Accept: application/json`. The visitor's
   session cookie is the only credential.
3. Your endpoint authenticates the cookie, mints a token for that user
   with [`@guuey/widget-auth`](https://www.npmjs.com/package/@guuey/widget-auth),
   and responds `200` with:

   ```json
   { "token": "<the signed token>" }
   ```

4. The page uses that token for the chat. Conversation history, memory,
   and profile all key off the identity your endpoint asserted — the same
   identity every other Guuey surface derives from that token; how that
   works is on [The identity model](/concepts-identity/).

There is **no guest fallback** on this path: if your endpoint can't be
reached or answers anything but the contract above, the page shows a
clear "sign-in unavailable" notice rather than silently dropping your
visitor into an anonymous session. When a token expires, the page
silently re-queries your endpoint — the visitor's session cookie is still
in the browser, so renewal is just another fetch, no prompt. Only if that
fetch fails does the visitor see the "sign-in service didn't respond"
notice with a **Try again** button.

## Configure the URL

The endpoint is part of the app's **standalone page** settings (it only
ever runs on the agent's own page). Set it in the console under
**Settings → General → Standalone page**, or:

```bash
guuey apps update <appId> --identity-endpoint-url https://www.example.com/api/guuey-identity
```

The URL must be `https://`. It is stored on the app whatever the auth
mode, but only used when the app's authentication is bring-your-own.

## The endpoint

The same minting code as an identified embed's token endpoint — enrol the
app once with `guuey widget keys create`, then:

```ts
import { signUserToken } from "@guuey/widget-auth";

// Pseudocode for any Node backend — authenticate YOUR session first.
export async function handleGuueyIdentity(req, res) {
  const session = await readYourSession(req); // your cookie, your rules
  if (!session) return res.status(401).end();

  const { token } = await signUserToken(
    { userId: session.userId, name: session.name, email: session.email },
    {
      appId: process.env.GUUEY_APP_ID!,
      appSecret: process.env.GUUEY_APP_SECRET!,
      // The API origin, without `/v1` — or set GUUEY_API_URL.
      apiBaseUrl: "https://api.us-east-1.guuey.com",
    }
  );
  res.setHeader("Content-Type", "application/json");
  res.json({ token });
}
```

:::caution[Not the same route as an embed's token endpoint]
The minting code is shared, but the response shapes differ: an
[identified embed's](/embed/#who-the-visitor-is) `getToken` endpoint
returns the **raw token string**, while this page requires the JSON
`{ "token": … }` body above plus the CORS headers below. Pointing
`--identity-endpoint-url` at a plain-text endpoint fails opaquely — the
page just shows sign-in as unavailable — so give the standalone page its
own route (or vary the response shape on the request's `Accept` header).
:::

## The CORS contract

The request arrives cross-origin with credentials, so the browser holds
your response to the strictest CORS rules. Your endpoint must send **all
three** headers, exactly:

| Header                             | Value                                                                                             | Why                                                                                                                                                                                                                                                                                |
| ---------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Access-Control-Allow-Credentials` | `true`                                                                                            | Without it the browser discards the response of a credentialed request.                                                                                                                                                                                                            |
| `Access-Control-Allow-Origin`      | The **exact** requesting origin, e.g. `https://chat.example.com` — only after you've validated it | `*` is rejected outright by browsers when credentials are involved, and reflecting any `Origin` unvalidated turns your session cookie into a token mint for every site on the internet. Validate against the fixed list of hostnames you serve your agent on, and refuse the rest. |
| `Vary: Origin`                     | —                                                                                                 | You're returning a per-origin header; without `Vary` a shared cache can serve one origin's `Allow-Origin` to another and break sign-in intermittently.                                                                                                                             |

```ts
const ALLOWED_ORIGINS = new Set([
  "https://chat.example.com", // your custom domain(s), nothing else
]);

const origin = req.headers.origin;
if (origin && ALLOWED_ORIGINS.has(origin)) {
  res.setHeader("Access-Control-Allow-Origin", origin);
  res.setHeader("Access-Control-Allow-Credentials", "true");
}
res.setHeader("Vary", "Origin");
```

The request is a simple `GET` (no custom headers), so no preflight
`OPTIONS` handling is required. If you route it through middleware that
adds one anyway, answer the preflight with the same three headers.

A CORS refusal is deliberately invisible to the page — the browser
reports it only as a network failure — so if sign-in shows as
unavailable, check your endpoint's response headers in the browser's
network inspector first.

## Cookies across subdomains

- If your agent's custom domain shares a site with your session — say the
  page is `chat.example.com` and the endpoint `www.example.com` — the
  request is same-site, and default (`SameSite=Lax`) session cookies are
  sent. Most setups need no cookie changes.
- If they are different sites entirely, the cookie must be issued with
  `SameSite=None; Secure` to accompany the request — with all the caveats
  that setting carries. Keeping the agent's domain under the same site as
  your auth is the simpler path.