Authenticated visitor tools

Plan: all paid tiers (sits on top of Tools).

Tools that return per-user data — order status, billing, account state — must know who is asking. Wilow never invents that identity; you give it to us, and we forward it to your webhook so you can do the lookup against your existing auth.

If your tool's answer is the same for everyone (store hours, return policy, available slots), you don't need this — skip back to Tools.

Flag the tool as authed-only

In Tools → New tool, check Requires the visitor to be signed in for any tool that reads or mutates per-user data. The bot will then refuse to call the tool for anonymous visitors and tell them to sign in on your site instead. Don't leave this off for tools that touch user data. Anonymous visitors hitting a cancel_order tool is exactly the leak class to avoid.

Leave it off only for tools whose answer is the same regardless of who is asking.

Three ways to identify the visitor

Pick whichever fits your stack. They're alternatives, not stacked.

1. Static script attribute — works for server-rendered sites where the JWT is in the page HTML at render time:

<script
  src="https://usewilow.com/widget/loader.js"
  data-api-key="..."
  data-user-jwt="<JWT>">
</script>

2. SPA call after login — for single-page apps that fetch the JWT from your auth library after the page has loaded:

window.wilow.identify({ jwt: "<JWT>" });
// On logout:
window.wilow.identify({ jwt: null });
// One-time global augmentation — paste into a `wilow.d.ts` (or any
// ambient .d.ts) at the root of your project. Picks up automatically.
declare global {
  interface Window {
    wilow?: {
      identify(payload: { jwt: string | null }): void;
    };
  }
}
export {}; // forces this file to be a module so `declare global` works

// Then in your app code, with full type-safety:
window.wilow?.identify({ jwt: "<JWT>" });
window.wilow?.identify({ jwt: null });

window.wilow is set by the widget loader once it boots, which can race a fast SPA hydration on the first page load. The optional-chained window.wilow?.identify(...) keeps the call safe in that window — a missed early call is fine, the loader caches the latest identify and the next tool invocation picks it up.

3. Read from a cookie — for sites that already store the session JWT in a JS-readable cookie. Configure the cookie name in Widget → Privacy → Visitor JWT cookie name. The widget loader reads document.cookie for that name on every page load and forwards it. Cookie must NOT be HttpOnly (HttpOnly cookies are invisible to JavaScript by design — you'll need #1 or #2 in that case).

What we send to your webhook

Every tool call includes Authorization: Bearer <jwt> when the JWT is present. The JWT is your token, signed by you — we never inspect it, never store it, never log it. Your webhook validates it the same way you'd validate any bearer token from your own clients.

// Node.js / Express. Same pattern as your existing auth-protected endpoints.
import jwt from "jsonwebtoken";

app.post("/wilow/tools/billing", express.raw({ type: "application/json" }), (req, res) => {
  // 1. Verify the Wilow signature (proves the request came from us).
  // ... see /docs/en/webhook-security#verifying-the-signature

  // 2. Verify the visitor JWT (proves who the visitor is).
  const auth = req.header("Authorization") ?? "";
  const token = auth.startsWith("Bearer ") ? auth.slice(7) : null;
  if (!token) return res.status(401).json({ error: "Sign-in required" });
  let user;
  try {
    user = jwt.verify(token, process.env.JWT_SECRET);
  } catch {
    return res.status(401).json({ error: "Invalid token" });
  }

  // 3. Now you know the user — do the lookup.
  const call = JSON.parse(req.body.toString("utf8"));
  const balance = lookupBilling(user.id, call.arguments);
  res.json(balance);
});

Do / don't

  • Do verify both the Wilow signature AND the visitor JWT on every request. Signature alone proves the call came from us; JWT alone proves who the visitor is. Either alone is insufficient.
  • Do treat the JWT exactly like you'd treat one received directly from your own frontend — same expiry, same audience claim, same revocation rules.
  • Don't return per-user data without checking the JWT, even if requiresAuthedUser is checked. The flag is a defence-in-depth gate, not a substitute for endpoint-side auth.
  • Don't trust the tenantId field in the request body for authorisation decisions — it's part of the body, signed by us as a whole, but only tells you which Wilow account called your hook (which is always you). The visitor identity is in the JWT.
  • Don't put a static API key in the Authorization header via custom headers — that header is reserved for the visitor JWT forward. We drop your custom Authorization header to prevent this.

Troubleshooting

JWT not landing on your webhook? Walk the troubleshooting checklist — usually it's an HttpOnly cookie or a case-mismatched cookie name.