Pairing — connect a user's browser
Before OpenErrand can run an errand for one of your users, that user's browser must be
paired to (your tenant, that user) once. Pairing binds an identity — it does not
move any credentials. This page explains the one-click connect flow, what makes it safe,
and the fallbacks.
TL;DR. You drop in OpenErrand's hosted connect button (one
<script>tag) and provide one backend route that mints a pairing token. OpenErrand owns the rest — the popup, the handoff, and the Connect/Connected state. Your own site needs no browser-extension allowlisting — onlyopenerrand.apptalks to the extension. Your coding assistant can generate the token route + the script tag with thescaffold_web_pairingMCP tool.
#The flow
Your app (app.acme.com) OpenErrand The user
---------------------- ---------- --------
1. user clicks the embed button
2. server: POST /pairing-tokens ──▶ relay mints a single-use,
{ userId } (Bearer API key) 2-minute token bound to
(tenant, userId)
3. embed.js opens a popup window
openerrand.app/connect.html ────▶ connect page:
#token=… • POST /pairing-tokens/describe
→ verified app name/logo
• sendMessage(extension, …) ──▶ extension parks it,
prompts: "Allow Acme
to run tasks in your
browser?"
◀── 4. user clicks Approve
relay redeems the token,
binds (tenant, userId)
5. extension closes the popup; the button now reads "OpenErrand Connected"
From then on, client.run({ errandId, userId, … }) from your backend can target that
user's browser. The binding persists until the user disconnects the app in the side panel.
#Step 1 — mint a pairing token (server)
When the user is signed into your app, your server (authenticated with your API key) mints a short-lived, single-use token bound to that user:
const r = await fetch(`${RELAY_HTTP}/pairing-tokens`, {
method: "POST",
headers: { authorization: `Bearer ${API_KEY}`, "content-type": "application/json" },
body: JSON.stringify({ userId: "dana" }),
});
const { pairingToken } = await r.json(); // single-use, expires in ~2 minutes
The API key never leaves your server — only the minted token crosses to the browser.
#Step 2 — drop in the connect button (browser)
You write no connect JS — OpenErrand ships a hosted widget. Add one script tag (pointing at the token route from Step 1) and a container:
<script src="https://openerrand.app/embed.js"
data-openerrand-token-url="/openerrand/pairing-token"
data-openerrand-tenant="acme"></script>
<div data-openerrand-connect></div>
The widget renders the button, fetches a token from your route, opens a small popup
window (not a full-page navigation), runs the handoff, and shows “Connect OpenErrand”
vs “OpenErrand Connected” on its own — the connected state is read from the extension
via a hidden openerrand.app status frame, so there's no extra backend. That's the whole
client side.
#Step 3 — the connect page does the handoff
openerrand.app/connect.html is the only origin allowed to message the extension
(it's the lone entry in the extension's externally_connectable list). It:
- calls
POST /pairing-tokens/describeto resolve whose token this is, so the page — and the approval prompt — shows a name the relay vouches for, never a string supplied in the URL. The name is the label of the API key that minted the token (set in your dashboard, one key per site → "Allow Acme Store to run tasks?"), falling back to your tenant display name and then the tenant id. It's validated against the consent policy (no control characters, no impersonating the OpenErrand trust frame). This call is read-only and does not consume the token. - hands the token to the extension, which parks it and prompts the user “Allow Acme to run tasks in your browser?” — it never pairs silently.
- on approval, the relay redeems the token and records the binding; the page returns the
user to your
returnURL.
If the extension isn't installed, the connect page shows an install prompt instead. If the token has expired or is malformed, it explains that and offers a way back.
#Why your site needs no allowlisting
A web page can only message an extension if the extension lists that page's origin in
externally_connectable — a list frozen into the published extension that only the
extension's author can change. Rather than ask every customer to get their origin added,
the handoff is centralized on openerrand.app (the embed widget opens the popup there),
so it works from any origin with zero extension configuration.
#What makes it safe — three independent gates
| Gate | Enforced by |
|---|---|
| The token is single-use and relay-validated (signed, ~2-minute expiry) | the relay, on redemption |
The handoff comes from the trusted openerrand.app origin |
the extension's externally_connectable allowlist |
| The user explicitly approves the named app | the extension's side-panel prompt — it never pairs silently |
Pairing establishes identity only. Destination-site credentials never touch OpenErrand — they're reused from the user's existing session or filled from their on-device vault at run time. See the security model.
#Fallbacks
- Manual paste. If the widget can't run (e.g. the popup is blocked), the user can paste the pairing token into the side panel's Connect an app field. Same gates apply.
- Enterprise auto-pair. Managed deployments pair without a per-user click using a tenant-signed identity assertion — see ENTERPRISE_DEPLOYMENT.md.
#Generate the code
The scaffold_web_pairing MCP tool emits the two pieces — the /openerrand/pairing-token
server route and the <script> tag for OpenErrand's hosted connect button — wired to your
errand. See the integration guide for the end-to-end walkthrough.