Skip to content

For developers

Let the assistant act for a signed-in user.

Out of the box, the assistant answers questions. With this integration it answers "what's my balance?" and does "reschedule my booking" — acting as one specific logged-in user of your product, without a real credential ever reaching the language model.

~1 day of developer time2 endpoints + 1 small serviceNo SDK — plain HTTPGrowth plan and up

Prefer to learn by building? The worked example walks the whole thing through, end to end, for one concrete app — plus the failure modes to expect.

01 — Why

What this enables, and when you need it

The test is simple: does the question contain the word my?

A normal AI Action calls your system — enough for "what are your hours" or "look up order #4182", anything where the customer supplies the identifying detail. It breaks down the moment the answer depends on who is asking, because the widget only knows an anonymous session id. This integration is what closes that gap.

The customer asks…Needs identity?Use
"What are your prices?"NoKnowledge base
"Where is order 4182?"NoA normal AI Action
"What's my plan and balance?"YesThis integration
"Cancel my Thursday booking"YesThis integration
"Add this word to my list"YesThis integration
What you'll build

Three things, all on your side, all under your control: a mint endpoint, an exchange endpoint, and a small Bridge service. On the Scode side there is no code — just actions you register in the dashboard. The rest of this guide is those five steps in order.

02 — Architecture

Anatomy at a glance

One opaque token travels from your logged-in page, through Scode, to your Bridge — and a real credential never leaves your servers.

  1. Your web appyou

    A signed-in user opens the chat. The widget quietly asks your API for a short-lived, opaque handoff token that means "this user started a chat".

  2. The Scode widgetscode

    Forwards that token to Scode alongside each message, as endUserToken. One extra embed attribute — nothing else about the widget changes.

  3. Scode's backendscode

    Holds the token in a server-side context and injects it into your action's request as a header via {{ctx:end_user_token}}. The model never sees it; it is never logged.

  4. Your Bridgeyou

    Receives the header, swaps the opaque token for a real user token server-to-server, checks the requested operation against an allowlist, and calls your own API as that user.

  5. The reply comes backscode

    Your API's result — including a business refusal like "too late to cancel" — flows back as data, and the assistant explains it in the customer's language.

The one idea to hold onto

The assistant never chooses a URL. It chooses from a short list of named operations you approved, each mapped to one route you verified. That single constraint is what makes it safe to put a language model in front of a user's account.

03 — Security model

Why an opaque token, and not just a JWT

The obvious shortcut is to pass the user's real session token through the widget. Do not. An AI Action's request template can only interpolate values the model supplies, so that design hands a live credential to a language model — and to anything that can influence it, including a prompt injection hidden in a document the model just read.

The handoff token is deliberately weak in four ways, and each one matters:

opaque
No readable claims. Reading it teaches an attacker nothing — not who the user is, not what they can do.
short-lived
Minutes, not hours. Five is a good default. A leaked token is worthless almost immediately.
single-audience
The aud claim is checked on redemption. It is rejected everywhere except your exchange endpoint — it cannot be replayed against your main API.
not a credential alone
Redeeming it also requires your bridge secret, which only your servers hold. The token by itself opens nothing.

What Scode guarantees on its side

  • The token resolves only through a separate {{ctx:...}} namespace — never from model-supplied arguments — so a model cannot inject its own value even if it tries.
  • It is never persisted to the action log, never returned to the model, never written into conversation history.
  • It renders into headers and the request body only — never a URL, so it cannot leak through access logs, proxy logs, or a Referer header.
  • It is released only to hosts you register on the platform's IdentityAllowedHosts list. An action pointed anywhere else receives an empty header — so a mistaken or malicious action can't exfiltrate identity.

The threat model, stated honestly

✓ What is contained

  • The model, and anything it reads, never touches a real credential.
  • An action a tenant admin points at their own server gets no token.
  • A stolen handoff token can't hit your main API — wrong audience.
  • The Bridge only ever performs operations you allowlisted.

! What you still own

  • If a handoff token leaks within its lifetime, the holder can invoke your allowlisted operations as that one user, for a few minutes.
  • So: keep the allowlist minimal, keep the TTL short, and gate destructive operations behind a confirmation in your API.

04 — Reference

The handoff token, in detail

You can implement both sides in any language in an afternoon — there is no library to adopt. This is the entire wire format:

// The handoff token is NOT a JWT. It is a signed, opaque blob:

  base64url( payloadJson ) + "." + base64url( HMAC_SHA256(payloadJson, SECRET) )

// payloadJson — the only fields, nothing readable or reusable:
{
  "sub": "a1b2c3d4-…",      // the user id you will act as
  "aud": "scode-bridge",   // audience — rejected anywhere else
  "exp": 1784562089,        // unix expiry — keep it short (≈5 min)
  "jti": "80416893…"       // unique id, so it can be one-time if you want
}

Verifying one — the whole algorithm

// Verifying a handoff token — 20 lines, any language. No JWT library needed.
function validate(token, expectedAud, secret) {
  const [body, sig] = token.split(".")
  const expected    = base64url(hmacSha256(body, secret))
  if (!constantTimeEquals(sig, expected))  return null   // forged or tampered

  const p = json(base64urlDecode(body))
  if (p.aud !== expectedAud)               return null   // wrong audience
  if (now() > p.exp)                       return null   // expired
  return p.sub                                           // the user id, verified
}
Base64url, not base64

Use URL-safe base64 (+-, /_, drop the = padding) so the token is safe in a header. Compare the signature in constant time — a byte-by-byte early-exit comparison leaks the secret to a patient attacker.

05 — Your side

Step 1 · The mint endpoint

One authenticated route that returns a token for the current user.

// POST /api/auth/support-handoff-token   — mint for the CURRENT user
// Put this where BOTH your short-lived access token AND your long-lived refresh
// credential can reach it. See "the credential-lifetime trap" below — this is the
// single most common way the whole integration silently dies.

[HttpPost("support-handoff-token")]
public IActionResult Mint()
{
    // 1) fast path — the access token is still alive
    var userId = CurrentUserIdOrNull();

    // 2) fallback — access token gone, but the session lives on in the refresh cookie.
    //    Validate it READ-ONLY. Do NOT rotate: rotating could burn a token the SPA is
    //    about to use, and reuse-detection would then log the user out. A support chat
    //    must never be able to sign someone out.
    if (userId is null)
        userId = ValidateRefreshCookieWithoutRotating(Request);

    if (userId is null)              return Unauthorized(new { code = "not_signed_in" });
    if (IsBanned(userId))            return Unauthorized(new { code = "account_banned" });
    if (IsAdmin(userId))             return Forbid();   // support path must never act as admin

    var token = HandoffToken.Mint(userId, aud: "scode-bridge", ttl: 300s, HANDOFF_SECRET);
    return Ok(new { token, expiresAtUtc = now.AddSeconds(300) });
}
The credential-lifetime trap — read this twice

The mint endpoint must stay reachable for as long as the session lasts. If it is authenticated only by a short-lived access token or cookie, it starts returning 401 the moment that expires — typically an hour in. The widget then silently sends no identity, and every action fails as an anonymous user while the person is still perfectly logged in.

It is invisible when it happens: the assistant simply starts saying it doesn't know who you are, and usually asks the customer for an internal id they have no way of knowing. This is the single most common way this integration breaks. Put the endpoint where your refresh credential is valid, or accept either credential — and test the expiry case before you ship.

CORS, precisely

The widget calls this cross-origin with credentials, which is stricter than a normal fetch. You need:

  • An exact Access-Control-Allow-Origin — the wildcard * is rejected for credentialed requests.
  • Access-Control-Allow-Credentials: true.
  • Your session cookie readable from the calling page — the simplest setup is the same registrable domain with SameSite=Lax.

06 — Your side

Step 2 · The exchange endpoint

Server-to-server only. It turns a valid handoff token into a real, short-lived user token your own API already understands.

// POST /api/support/exchange   —  server-to-server ONLY (the Bridge calls this)
// Never expose it to a browser. It turns a handoff token into a real user token.

[HttpPost("exchange")]
public async Task<IActionResult> Exchange([FromBody] ExchangeRequest req)
{
    // constant-time compare — a plain == leaks the secret one byte at a time
    if (!FixedTimeEquals(Request.Headers["X-Bridge-Secret"], BRIDGE_SECRET))
        return Unauthorized(new { code = "bridge_auth_failed" });

    if (!HandoffToken.Validate(req.HandoffToken, "scode-bridge", HANDOFF_SECRET, out var userId))
        return Unauthorized(new { code = "handoff_invalid" });

    var user = await FindUser(userId);
    if (user is null)          return NotFound(new { code = "user_not_found" });
    if (user.IsAdmin)          return Forbid();        // second line of defence

    var (accessToken, exp) = Jwt.IssueFor(user);       // your normal user token
    return Ok(new { accessToken, userId, expiresAtUtc = exp });
}
  • Compare the shared secret in constant time.
  • Verify signature, audience, and expiry — and reject on any failure without revealing which one failed.
  • Refuse privileged accounts, twice — here and at mint. If an admin ever opens the support chat, the support path must not become a way to act with admin rights.
  • Issue the shortest user token your API accepts. The Bridge uses it once, immediately.

07 — Your side

Step 3 · The Bridge

A small service — the only thing Scode ever calls. It exists to be a narrow, auditable door into your API, and nothing more.

// The Bridge — the ONLY thing Scode ever calls. Its whole job is to be a narrow,
// auditable door into your API. A few hundred lines total.

// The allowlist IS the security boundary. Every operation → one verified route.
var allow = {
  // reads (no confirmation needed)
  get_profile:      { GET,   "/api/users/me" },
  list_bookings:    { GET,   "/api/bookings/me" },
  // writes — your own API still enforces every business rule
  book_session:     { POST,  "/api/bookings",                    forwardBody: true },
  cancel_booking:   { POST,  "/api/bookings/{bookingId}/cancel", pathParams: ["bookingId"] },
}

app.post("/op/:operation", async (req, res) => {
  const op = allow[req.params.operation]
  if (!op) return res.status(403).json({ ok: false, error: "operation_not_allowed" })
                                                       // unknown op → 403, NEVER a passthrough

  const handoff = req.header("X-Scode-Handoff")
  if (!handoff) return res.status(401).json({
    ok: false, error: "missing_identity",
    message: "The user is not signed in. Ask them to sign in and retry —" +
             " do not ask them for an account or booking id." })

  // 1) swap the opaque token for a real user token, server-to-server
  const { accessToken } = await exchange(handoff)      // POST /api/support/exchange + X-Bridge-Secret

  // 2) fill {path} params from the body, then call your API AS that user
  const path = fillParams(op.path, req.body, op.pathParams)
  const r = await fetch(baseUrl + path, {
    method: op.method,
    headers: { Authorization: `Bearer ${accessToken}` },
    body: op.forwardBody ? JSON.stringify(req.body) : undefined,
  })

  // 3) relay the result — including a business refusal — back as DATA, capped so a
  //    huge payload can't blow up the model's context
  const body = (await r.text()).slice(0, 8000)
  res.json({ ok: r.ok, status: r.status, data: safeJson(body) })
})

Five rules worth keeping

  1. Never a passthrough. An unknown operation is a 403, not a proxied request. The allowlist is the security boundary — the moment it forwards arbitrary paths, you have handed a URL chooser to a language model.
  2. Map to routes you verified. Check each path against your actual controllers, not what you remember them being. Assumed routes are a reliable source of silent breakage.
  3. Cap the response (8 KB is plenty). Whatever you return is fed straight into the model's context.
  4. Relay business refusals as data. When your API says "too late to cancel", pass that message through with ok:false so the assistant can explain it warmly — instead of surfacing a bare failure it will narrate badly.
  5. Return an actionable message on missing identity — literally tell the assistant to ask the user to sign in and retry. Without it, the assistant improvises and typically asks for an internal id the customer can't possibly have.

08 — Your side

Step 4 · Embed the widget

Add one attribute to the standard embed. Everything else is unchanged.

<!-- on YOUR pages, where the user is already signed in -->
<script
  src="https://ai.scode.iq/widget/scode-widget.min.js"
  data-api-key="sk_your_widget_key"
  data-base-url="https://api.ai.scode.iq"
  data-end-user-token-url="https://api.yourapp.com/api/auth/support-handoff-token"
  data-language="auto"
  defer></script>

What the widget does with it

// What the widget does with data-end-user-token-url, so you know what to build against:

async function getToken() {
  if (cached && now() < cached.expiresAt - 30_000) return cached.token   // reuse, minus 30s skew
  if (inFlight) return inFlight                                          // single-flight: 2 sends → 1 mint
  inFlight = fetch(tokenUrl, {
    method: "POST",
    credentials: "include",          // sends YOUR session cookie — this is the whole trick
    headers: { Accept: "application/json" },
  }).then(handle)
  return inFlight
}
// 401 / network error → returns null → the widget just chats anonymously.
// Cache is also hard-capped at 90s so an SPA account-switch can't keep the old token long.

// SPA hook: when your user signs out or switches account WITHOUT a page reload, call
window.ScodeChat.identify()   // drop the cached token immediately

If the token call fails or the visitor is signed out, the widget simply sends no identity and behaves as an anonymous chat — so your public pages keep working untouched. There is no "half-broken" state.

Single-page apps

If a user can sign out or switch accounts without a page reload, call window.ScodeChat.identify() at that moment (invalidateIdentity() is the same call under a more literal name). The widget also hard-caps the cache at 90 seconds as a backstop, but calling it explicitly closes the window immediately — important because a stale token would let the assistant act as the previous user.

Placing it inside your own product

A floating bubble is rarely what you want inside a logged-in application. The widget can render inside an element you control instead, so the chat becomes part of your layout — a support panel in a settings page, a tab in an account area, a dedicated route.

<!-- your layout owns the box; the chat fills it -->
<div id="support-chat" style="height: 560px"></div>

<script
  src="https://ai.scode.iq/widget/scode-widget.min.js"
  data-api-key="sk_your_widget_key"
  data-base-url="https://api.ai.scode.iq"
  data-end-user-token-url="https://api.yourapp.com/api/auth/support-handoff-token"
  data-mode="inline"
  data-target="#support-chat"
  defer></script>

Add data-mode="inline" and data-target pointing at your container (data-mode="fullpage" for a whole route). Two things worth knowing before you ship it:

  • The container needs a height. The chat fills it. With no height we apply a default and log a note; we deliberately do not force a minimum, because that would overflow any container shorter than it and paint over your page.
  • Rendering it late is fine. If the target does not exist when our script runs — the normal case in an SPA — we watch for it and mount as soon as it appears. If it never appears we log an error and fall back to the floating launcher, so your users still have a way to reach support. Pass data-target-fallback="none" if you would rather have no chat at all than an unexpected bubble.

To drive it from your own UI entirely, add data-launcher="none" and call window.ScodeChat.open() from your button. ScodeChat.on('escalated', …) and on('message:received', …) let you mirror conversation state into your own analytics or UI.

Content Security Policy

If your app sends a CSP, it must allow the widget script and the API it talks to — script-src for the bundle's host and connect-src for https://api.ai.scode.iq (the chat streams over SSE). The widget injects no inline <style> — it uses constructed stylesheets — so it survives a strict style-src untouched. Worth checking while your policy is still report-only: a missing connect-src entry only breaks the chat once you enforce it.

09 — In the dashboard

Step 5 · Register the actions

Now tell the assistant what it may do. In the tenant dashboard go to AI Actions → New action and choose Connect your own system. Create one action per bridge operation.

A read action

{
  "name": "get_my_profile",
  "description": "Get the signed-in user's OWN profile: plan, balance, status. Use
                  when they ask about THEIR account. Arabic: حسابي، رصيدي، خطتي.",
  "actionType": "api_call",
  "endpointUrl": "https://bridge.yourapp.com/op/get_profile",
  "httpMethod": "POST",
  "headers": { "X-Scode-Handoff": "{{ctx:end_user_token}}" },
  "responseTemplate": "{{response}}",
  "parameters": []
}

A write action — with a body and typed parameters

{
  "name": "book_appointment",
  "description": "Book an appointment for the signed-in user. First call list_providers
                  for a providerId. Your API validates availability and refuses with a reason to relay.",
  "actionType": "api_call",
  "endpointUrl": "https://bridge.yourapp.com/op/book_appointment",
  "httpMethod": "POST",
  "headers": { "X-Scode-Handoff": "{{ctx:end_user_token}}" },
  // {{json:...}} quotes a value safely. A raw {{durationMinutes}} where the model
  // supplies "thirty" would produce INVALID JSON; {{json:}} yields "thirty" or 30 correctly.
  "requestBodyTemplate": "{\"providerId\":\"{{providerId}}\",\"minutes\":{{json:durationMinutes}}}",
  "parameters": [
    { "name": "providerId",     "type": "string",  "required": true },
    { "name": "durationMinutes", "type": "integer", "required": true }
  ]
}

The fields that matter

endpointUrl
Your bridge operation URL. One action per operation. This is the host that must be on IdentityAllowedHosts.
headers
The identity header, set to {{ctx:end_user_token}}. This one line is the entire integration on the Scode side.
description
The only free text the model reads when choosing a tool. Say when to use it, when not to, and include the phrasings your users actually type. See §11.
parameters
Only what the model must supply. Identity is not a parameter — it arrives server-side, out of the model's reach.
requestBodyTemplate
For writes. Use {{json:name}} for anything non-string so a model-supplied value can't produce invalid JSON.
responseTemplate
{{response}} feeds the whole reply to the model, or {{$.path.to.field}} plucks one value out.

Then switch it on

  1. In Settings → AI, enable AI Actions for the tenant.
  2. Ask the Scode team to add your bridge host to AiActions:IdentityAllowedHosts. Until then the identity header arrives empty by design — this is the safety default, not a bug.
  3. Test each action with the built-in test button, then end-to-end as a real signed-in user.

10 — Design

Designing your operation allowlist

The allowlist is where you decide exactly how much reach to give the assistant. Treat it like an API surface you'll live with, because you will.

✓ Do

  • Start with reads only. Ship "what's my…" first; add writes once you trust the logs.
  • Give each operation a clear verb-noun name: get_profile, cancel_booking.
  • Map one operation to one real, verified route.
  • Keep path ids as {pathParams} the Bridge fills from the body — never let the model build a URL.

✗ Don't

  • Expose a generic /op/proxy?url=…. That's the whole thing you're avoiding.
  • Add an operation "just in case". Every one is attack surface for the token's lifetime.
  • Rely on the Bridge for permission. It adds reach; your API still enforces rules.
  • Return more than the assistant needs — big payloads cost context and can leak fields.
Reads and writes are different risk classes

A read that leaks is an information problem; a write that misfires changes a customer's data. Keep destructive operations (cancel, delete, pay) behind a confirmation step in your API, so even a perfectly-authenticated call needs a second signal before it commits.

11 — The craft

Writing descriptions the model actually follows

This is the highest-leverage paragraph in the whole integration, and the one teams skip.

When the assistant decides which tool to call, the only free text it sees is each action's name and description. The dashboard also has a "trigger phrases" field — those are not sent to the model. They help other parts of the product, but they do nothing for tool selection. If your users ask in Arabic, the Arabic has to be in the description.

✗ Weak description

  • "Get appointments."
  • Says nothing about when to reach for it, so the model guesses — and guesses wrong next to a similar action.

✓ Strong description

  • "Get the customer's OWN upcoming appointments — date, time, provider, status. Use when they ask ‘do I have anything booked’, ‘when's my next appointment’, or شنو مواعيدي. Arabic: مواعيدي، حجوزاتي، عندي حجز."
  • Say when NOT to use it. Overlapping descriptions are the main cause of the assistant picking the wrong operation.
  • Spell out house terms. The model can't infer that your branded plan name, loyalty currency, or in-app feature means what it means — if a word only makes sense inside your product, define it right here.
  • Name the fields the model must supply and where they come from ("first call list_providers for a providerId").
  • State the authoritative field when your data has traps — e.g. "use durationMinutes, not the raw column, which can disagree."
  • Editing a description changes behaviour. Re-test after every edit, and make sure your registration script actually pushes description changes — not just new actions.

12 — Prove it works

Testing

Three levels, cheapest first.

1 · The dashboard test button

Every action has a test button that fires it with sample arguments — no customer, no chat. It confirms the URL, method, and template render. Note it injects no identity token, so an identity-gated operation will come back 401 from your Bridge — that's expected here; it proves the call reached the Bridge.

2 · The chain, from a terminal

# Prove the whole chain from a terminal, no browser needed.
API=https://api.yourapp.com ; BRIDGE=https://bridge.yourapp.com

# 1) sign a user in (however your API does it), keeping the cookie jar
curl -s -c jar.txt -X POST $API/api/auth/login -d '{"email":"…","password":"…"}'

# 2) mint a handoff token as that user
HT=$(curl -s -b jar.txt -X POST $API/api/auth/support-handoff-token | jq -r .token)

# 3) call the Bridge with it — should return that user's real data
curl -s -X POST $BRIDGE/op/get_profile -H "X-Scode-Handoff: $HT"

# 4) no identity → must be refused, never served
curl -s -X POST $BRIDGE/op/get_profile          # → 401 missing_identity

3 · The failure test that matters most

A green happy-path is not enough — the failure that actually bites in production is a credential quietly expiring. Simulate it on purpose:

# THE test that catches the #1 production break BEFORE your users do.
# Drop the short-lived access cookie, keep only the refresh cookie, then mint again.

grep -v access_token jar.txt > expired.txt        # simulate 60-min access-token expiry

curl -s -o /dev/null -w "%{http_code}\n" \
  -b expired.txt -X POST $API/api/auth/support-handoff-token
# MUST be 200. If it's 401, your mint endpoint dies an hour into every user's session,
# the widget silently sends no identity, and the bot starts saying "I don't know who you are".
Read latency as a signal

When you test through the live chat, watch the timing. An action-backed answer that arrives in ~1 second is a cached answer, not a fresh call; a real action turn takes a few seconds (retrieval → tool call → reply). If a wrong answer keeps coming back instantly, it's a stale cache entry — purge the tenant's answer cache and it regenerates.

13 — Cutover

Going to production

Build and test against staging with scrubbed data — never production accounts. When you're ready to flip:

  1. Point the actions' endpointUrl at your production bridge host, and update data-end-user-token-url on the production embed.
  2. Add the production bridge host to IdentityAllowedHosts. Miss this and identity silently stops flowing — the header goes empty and every operation 401s, exactly as if you were signed out.
  3. Rotate to production secrets — a distinct handoff secret and bridge secret, in environment variables, in no repository.
  4. Re-run the expiry test against production, as a real user.
  5. Confirm your API still enforces every business rule under the bridge's token. The bridge adds reach, never permission.
The cutover gotcha

The most common cutover mistake is updating the actions' host but forgetting the allowlist — or vice-versa. Both must name the same production bridge host. Test one real operation end-to-end before you announce anything.

14 — When it misbehaves

Troubleshooting

SymptomUsual cause & fix
Bot says it doesn't know who you are, while you are signed inThe mint endpoint is returning 401 — almost always because it outlived its access cookie. Open it directly in the browser's network tab. Fix: mint where the refresh credential is valid.
Works signed-in on one page, not anotherCookie scope. The mint call is credentialed and cross-origin — check exact-origin CORS and Allow-Credentials: true, and that the cookie is readable from that page.
Every operation returns missing_identityThe header arrived empty. Either the widget sent no token, or your bridge host is not on IdentityAllowedHosts. In the dashboard, the action's test button reaching a 401 confirms the URL is right.
identity_rejected from the BridgeThe token arrived but your exchange refused it — expired (they're short-lived), wrong audience, or signed with the wrong secret. Reloading the page mints a fresh one.
A wrong answer keeps repeating; rewording it fixes itA cached answer from when the integration was briefly broken. The answer cache keys on the question, not the user. Purge the tenant's answer cache; identity-bearing and action-driven answers aren't cached going forward.
The assistant picks the wrong operationDescriptions overlap or are vague. Say plainly when not to use each one, and keep the operation list short. See §11.
A write action produces a malformed-body errorA non-string value interpolated raw. Use {{json:field}} so "thirty" or 30 both serialise to valid JSON.
The bot quotes a number that's wrong for this userTwo fields in your payload disagree and the model picked the wrong one. Name the authoritative field in the action's description.
An answer arrives in ~1 secondCache hit, not a fast model. Useful for telling "stale" from "broken": a real action turn takes a few seconds.

15 — Reference

Reference tables

Template placeholders

Anywhere in an action's URL, headers, or body:

{{name}}
A parameter the model supplied. Resolves from arguments only.
{{json:name}}
Same, JSON-encoded — use for any non-string value in a request body.
{{ctx:name}}
A server-side value — the identity token lives here as end_user_token. Never from model arguments, never in a URL, never logged.
{{response}}
In responseTemplate: the whole response body, fed to the model.
{{$.a.b}}
In responseTemplate: pluck one nested field out of the JSON response.

Widget embed attributes

data-api-key
Required. Your widget key from the dashboard.
data-base-url
The Scode API base, https://api.ai.scode.iq.
data-end-user-token-url
The integration. Your mint endpoint. Omit it and the widget is a normal anonymous chat.
data-language
auto · ar · en.
data-brand
Optional brand slug for multi-brand tenants.
data-color · data-position · data-welcome · data-session-ttl-hours
Cosmetic / behavioural tweaks. See the Owner's guide.

Bridge error codes (a suggested convention)

missing_identity · 401
No handoff header. Ask the user to sign in.
identity_rejected · 401
Exchange refused the token (expired / wrong secret). Reload to re-mint.
operation_not_allowed · 403
Operation isn't on the allowlist.
ok:false + status + data
Your API refused for a business reason — relayed so the assistant can explain it.

16 — FAQ

Questions that come up

Do I need an SDK?

No. It's plain HTTP on both sides — two endpoints and one small service in whatever stack you already run.

Can the model ever see the real user token?

No. The real token is issued inside your Bridge, used once, and discarded. Only the opaque handoff token ever travels through Scode, and even that is kept out of the model's view.

What if the same person is signed in on two tabs?

Fine. Each tab mints its own short-lived token from the same session; validating one never disturbs the other. The mint is read-only — it never rotates or consumes your session.

Can I do this on WhatsApp or Telegram?

Not yet — those channels have no logged-in browser session to mint from. This pattern is for surfaces where your user is already authenticated in a browser: your web app.

How many operations should I expose?

The fewest that cover what customers actually ask. Ship reads first. Each operation is live attack surface for the token's lifetime, so "just in case" is the wrong instinct.

Where's a complete example?

The worked example builds the whole integration for one concrete app, in order, and walks through the failure modes you're most likely to meet.