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.
| Value | Looks like | Where it goes |
|---|---|---|
| Project ID | a UUID | your page, in the boot() call |
| Signing secret | a long random string | your server only — never the page |
| Allowed origin | https://portal.yourcompany.com | set 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:
expis two minutes. The assertion is exchanged for a session immediately; a longer life is a longer window for a stolen one.jtimust be unique. It is single-use — a replayed assertion is refused.org_idis the visibility boundary. Everyone sharing anorg_idcan see each other's requests. If that is not what you want, give each person their own.
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:
| Directive | Value |
|---|---|
script-src | https://cdn.beacon.inits.dev |
connect-src | https://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:
| Symptom | Almost always |
|---|---|
| No launcher at all | the origin is not on the allowlist (step 1) |
| Launcher appears, panel says requests are unavailable | the assertion was refused — check exp, and that jti is fresh each call |
| Console shows a CSP violation | step 4 |
| Launcher in the wrong corner | that 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
- No build step. It is an ES module; there is nothing to bundle.
- No stylesheet. Theming is a project setting, applied from the server.
- No layout work. The widget positions itself and never reflows your page.
- No configuration in code. Which tabs exist, what the form asks, where the widget appears, what customers may see — all of it is changed in the console by someone who is not you, without a deploy on your side.
- Nothing to do for the AI assistant. If an admin switches it on in the console, an Ask tab appears in the same widget, answering from that project's own published help content and handing over to a person on request. No key of yours, no extra call, and no change to this page.
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:
| Signed | Anonymous | |
|---|---|---|
| Work on your side | one endpoint | none |
| Customer sees their past requests | yes | no |
| Reporter on a ticket | verified, from your session | self-declared, marked unverified |
| Who can raise a request | your signed-in users | anyone who can load the page |
What you get by signing
- Customers see their past requests. This is the feature most of them notice, and it needs a verified identity.
- The name on the ticket is one your team can act on — replying to the address, or weighing a claim in a dispute. On an anonymous project every internal view marks the reporter unverified, so nobody relies on it by accident.
- The customer types nothing. No name, no email — you already know them.
- Rate limits attach to a person rather than to whoever is currently sending requests.
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.
Related
- Embedding the widget — the full contract: the signing format, every
boot()option, the events the widget emits, and what happens when things fail. - Identity handshake — code samples — step 2 and step 3 in your own stack, server side and browser side.