Beacon

Fifteen minutes, if your backend already knows who is signed in

Quick start

Getting Beacon into your product. Fifteen minutes if your backend can already tell you who is signed in.

There are two pieces of work, and only one of them is on your page. If you cannot identify your users — or would rather not — skip to anonymous mode and there is only one.


Before you start

You need three values from whoever set up your Beacon project. They live in the console under Settings, and none of them is a secret you have to protect on the page.

If nobody set one up yet, you can. Signing up creates your organisation and a project that is already active, with a support policy in place and a signing secret issued once at the end — keep that one. Your first task afterwards is step 1 below, because a new project's origin allowlist is deliberately empty.

ValueLooks likeWhere it goes
Project IDa UUIDyour page, in the boot() call
Signing secreta long random stringyour server only — never the page
Allowed originhttps://portal.yourcompany.comset in the console, before step 3

The signing secret is shown once — when the project is created, or when you rotate it under Settings → Signing secrets. There is no screen that will show it to you again, deliberately: a copy we could hand back is a copy somebody else could ask for.

The signing secret is the one that matters. Anyone holding it can mint an identity for any of your users and read that organisation's entire support history. It stays on your server.


1. Add your origin to the allowlist

In the console: Settings → Allowed origins, one origin per line, exactly as the browser sends it — scheme and host, no path, no trailing slash.

https://portal.yourcompany.com

Do this first. Until an origin is on the list the widget will not start on it, and the failure will not tell you that is why — an unrecognised origin gets the same response wherever it comes from.


2. Mint an assertion on your server

One endpoint. It answers only for the user who is already signed in to your product, and it signs a short-lived statement about them.

# Your framework, your session. The only Beacon-specific parts are the claims,
# the HMAC key, and the two-minute lifetime.
import hashlib, hmac, json, time, uuid, base64

BEACON_SECRET = os.environ["BEACON_SIGNING_SECRET"]   # never sent to the browser
KEY = hashlib.sha256(BEACON_SECRET.encode()).digest()

@app.post("/support-identity")
def support_identity():
    user = current_user()          # your session, not ours
    if user is None:
        abort(401)
    now = int(time.time())
    claims = {
        "sub": user.id,            # stable per person, opaque to us
        "name": user.full_name,
        "email": user.email,
        "org_id": user.organisation_id,   # decides whose tickets they can see
        "iat": now,
        "exp": now + 120,          # two minutes; see below
        "jti": str(uuid.uuid4()),  # one use only
    }
    return {"assertion": sign(claims, KEY)}

Three details that are not decoration:

The exact signing format is in the integration guide, and the same endpoint written out for Node, Python, PHP and Ruby — and wired into Express, Next.js, Django, FastAPI, Flask, Laravel and Rails — is in the code samples.


3. Add the script tag

<script type="module">
  import { boot } from "https://cdn.beacon.inits.dev/widget/v1/loader.js";

  boot({
    baseUrl: "https://api.beacon.inits.dev/api/v1/widget",
    projectId: "your-project-id",
    getAssertion: async () => {
      const res = await fetch("/support-identity", { method: "POST" });
      return (await res.json()).assertion;
    },
  });
</script>

That is the whole page-side integration. The widget renders into a shadow root, so your CSS cannot reach it and its CSS cannot reach yours.

Pin /v1/. Fixes and additions ship to it continuously and reach your users on their next page load; anything that would break an existing embed ships as /v2/ instead.


4. Content-Security-Policy

If you set a CSP — and you should — two directives need entries:

DirectiveValue
script-srchttps://cdn.beacon.inits.dev
connect-srchttps://api.beacon.inits.dev

If either hostname ever changes, you will be told in advance.


5. Check it

Load a page where a real user is signed in. You should see the launcher in the corner your project configured.

If nothing appears, work down this list — it is ordered by how often each one is the answer:

SymptomAlmost always
No launcher at allthe origin is not on the allowlist (step 1)
Launcher appears, panel says requests are unavailablethe assertion was refused — check exp, and that jti is fresh each call
Console shows a CSP violationstep 4
Launcher in the wrong cornerthat is a project setting, not a page setting — Settings → Widget

The widget never throws into your page. A failure it cannot recover from leaves your page exactly as it was — which does mean a silent failure looks like "nothing happened".


What you do not have to do


Do I have to identify my users?

No. A project can run in anonymous mode, and then step 2 disappears entirely — no endpoint, no signing secret, no getAssertion. Your page becomes a script tag and nothing else.

The customer types their name and email on the form instead, and both are recorded as unverified, because they are: anybody can type anybody's address.

<script type="module">
  import { boot } from "https://cdn.beacon.inits.dev/widget/v1/loader.js";

  // No getAssertion. On an anonymous project the widget does not ask for one —
  // not asks and ignores: minting an assertion nobody will read is a request, a
  // signature and a burned single-use id for nothing.
  boot({
    baseUrl: "https://api.beacon.inits.dev/api/v1/widget",
    projectId: "your-project-id",
  });
</script>

Set the mode in the console: Settings → Identity. The widget reads it from the server, so switching does not need a change on your page — and the widget treats an unknown or unreachable mode as signed, the stricter of the two, so a project that wanted signing cannot be opened unsigned by a config request that failed.

What it costs

Your customers lose their request history. An anonymous project is forced to ticket_visibility = none — not as a default you can change, but as a rule the system refuses to let you configure around. The Requests tab is empty, and it is empty at the endpoint, not by hiding a tab.

Without a verified identity there is nothing to check a request list against, so showing one would mean showing a customer's support history to whoever typed their email address.

The trade:

SignedAnonymous
Work on your sideone endpointnone
Customer sees their past requestsyesno
Reporter on a ticketverified, from your sessionself-declared, marked unverified
Who can raise a requestyour signed-in usersanyone who can load the page

What you get by signing

If you start anonymous and add identity later, existing tickets keep their self-declared reporters — they cannot be retro-verified, because there was never anything to verify them against.