HMAC examples by stack
Widget / HMAC examples

HMAC visitor token: examples for popular stacks

Ready-made snippets of the /api/supporthub/token endpoint for FastAPI, Django, Flask, Express, Next.js API, Laravel, Rails, Go, .NET and Spring Boot. The full reference with the payload format, the backend checks and edge cases is on the HMAC visitor token page.

What you need to do

  1. Get the project secret once, by copying it in SettingsChannelsWeb Widget (admin rights needed) or requesting it with an API key from SettingsAPI Keys, and put it in your backend’s env:
    http
    GET /api/v1/widget/signing-secret
    Authorization: Bearer sk_xxx
    
    200 OK
    { "secret": "a random 43-character string" }
  2. Implement a GET /api/supporthub/token endpoint on your side using one of the templates below. The endpoint must:
    • authenticate the current user (with a session cookie, a JWT, anything);
    • take a stable user_id (the PK from your database, a slug, etc.);
    • build the payload {"user_id": "...", "exp": <unix>};
    • sign the raw JSON bytes with HMAC-SHA256;
    • return { "token": "<base64url(payload)>.<hex(sig)>" }.
  3. Uncomment the fetch block in the install snippet and remove the loadWidget(); line below it; otherwise the widget first loads without a token, and the second load, with the token, is ignored. The loader puts the token into data-visitor-token, and the widget sends it to SupportHub with every request.
  4. When the user signs out, call window.SupportHub.logout(): the widget forgets the token and starts a clean anonymous session, so the next person at that browser doesn’t see someone else’s conversations (more in Identifying visitors).

Implementation examples

All examples use HMAC-SHA256 over the raw JSON bytes (not the base64 string), base64url encoding of the payload without the = padding, and compact JSON without spaces (separators=(",", ":") or the equivalent). The backend requires neither compact JSON nor dropping the =: it checks the signature over exactly the bytes encoded in the token. The TTL is 1 hour; set it to taste, just don’t make it permanent.

app/api/supporthub.pypython
import base64, hashlib, hmac, json, os, time
from fastapi import APIRouter, Depends, HTTPException

router = APIRouter()
SECRET = os.environ["SUPPORTHUB_SIGNING_SECRET"].encode()

@router.get("/api/supporthub/token")
async def supporthub_token(current_user = Depends(get_current_user)):
    if not current_user:
        raise HTTPException(401, "Unauthorized")

    payload = json.dumps(
        {"user_id": str(current_user.id), "exp": int(time.time()) + 3600},
        separators=(",", ":"),
    ).encode()
    sig = hmac.new(SECRET, payload, hashlib.sha256).hexdigest()
    payload_b64 = base64.urlsafe_b64encode(payload).rstrip(b"=").decode()
    return {"token": f"{payload_b64}.{sig}", "expires_in": 3600}

Test run without a backend

To try the API by hand before you build the endpoint, sign a payload, for example in a Python REPL, and put it in the HTML as the data-visitor-token attribute:

python
import base64, hashlib, hmac, json, time

SECRET = "PASTE_SECRET_HERE"   # your project secret
payload = json.dumps(
    {"user_id": "demo-42", "exp": int(time.time()) + 3600},
    separators=(",", ":"),
).encode()
sig = hmac.new(SECRET.encode(), payload, hashlib.sha256).hexdigest()
print(base64.urlsafe_b64encode(payload).rstrip(b"=").decode() + "." + sig)
html
<script async
        src="https://support.forestsnet.com/widget-bundle?ws=YOUR_WS"
        data-visitor-token="OUTPUT_OF_THE_PYTHON_ABOVE"></script>

Next steps

  • Full HMAC reference: the payload format, what the SupportHub backend checks and the 401s it returns, edge cases (an expired token, signing in mid-session, signing out and switching accounts, several tabs), rotating the secret, how to check a token with curl.
  • Identifying visitors: what to pass in data-* next to the token (email, name, phone, Telegram ID).
  • Troubleshooting: common widget problems, including a token the backend rejects (401).
Was this page helpful?