Skip to main content
This page is for whoever reviews WireDesk before it goes on your site. It covers what is public and what is secret, where each control is enforced, and what a visitor, the host page and the agent can each reach.

The widget key is public

A widget key (wk_ followed by 32 hex characters) sits in your page source. It identifies one widget and lets the holder chat with one agent. It does not open the dashboard, conversations or any other data. Treat it like a publishable key, not a secret. Three controls limit what someone can do with a copied key:
  • Allowed domains stop it working on other sites.
  • Rate limits stop it being scripted.
  • Your workspace spend cap puts a hard limit on what any abuse costs. See spend cap.

Allowed domains

A widget starts with no list, and a widget without a list works on any site. Once you save a list on Agents → your agent → Install → Allowed domains, it is enforced in three places. You can list up to 20 domains, and each covers its subdomains: example.com matches example.com, www.example.com and shop.example.com. localhost is accepted for local development. More detail on each:
  • CORS on the config endpoint. With no list, the response carries Access-Control-Allow-Origin: * and Cache-Control: public, max-age=60. Once a list exists, it echoes the caller’s own origin, adds Vary: Origin and switches to Cache-Control: private, max-age=60, so a shared cache cannot hand one site’s answer to another. The config fetch is made with credentials omitted.
  • Frame ancestors. For each listed domain the policy allows https://domain, https://*.domain, http://domain:* and http://*.domain:*, so a developer’s local http build still works. No X-Frame-Options is sent on this path, because that header cannot express a list.
  • Chat requests without an Origin. The panel’s own polling requests are same-origin and carry no Origin header. They are accepted only when the browser marks them Sec-Fetch-Site: same-origin, or, on browsers that do not send that header, when the Referer is WireDesk’s own origin. A caller with neither header is refused.
The config endpoint lets a request with no Origin header through. Only browsers can report which website is embedding the widget, and a browser sends Origin on this cross-origin request. See Allowed domains for setup.

Verified visitors

Verified visitors are off by default. When you choose Generate a signing secret, the widget gets a per-widget secret: whs_ followed by 64 hex characters, from 32 random bytes. The dashboard shows it masked. Reveal, Rotate and Turn off need the admin or owner role, and each change is recorded in the activity log.
  • Signature: the HMAC-SHA256 of the user id, hex-encoded, keyed with that secret. Your server computes it. The secret never goes into the page.
  • Check: the server recomputes the digest and compares the two in constant time. A digest that is not 64 lowercase hex characters, or that does not match, makes the identity invalid. The identity is then ignored, and nothing in it is trusted.
  • Identity sent to a widget without a secret: treated as invalid and ignored.
  • Ownership: a verified conversation belongs to email:<address> when you send an email, or otherwise to the id you signed. Every chat endpoint checks the conversation’s owner against the verified identity or the visitor id. Reading or ending someone else’s conversation gets 404. Sending to it starts a new conversation instead.
  • Require verification: with this on, a message without a valid identity gets 403. An invalid signature is answered with “we couldn’t verify who you are — please reload the page”. A missing one gets “this chat is only available when you’re signed in”.
  • Rotate takes effect immediately. Deploy the new secret to your servers first.

Where the identity travels

  1. Your page gives widget.js the identity, through window.wiredeskSettings.user or wiredesk("identify", …).
  2. widget.js passes it to the panel in the iframe URL’s fragment (#wd=…), or by postMessage if the panel is already open. It never goes in a query string. Browsers do not send fragments to servers, so the identity stays out of access logs and Referer headers.
  3. The panel sends it to the chat endpoints in an X-Wd-Identity request header, as percent-encoded JSON.
The server caps the fields: id at 128 characters, name at 120, and email at 254. An invalid email is dropped.

Iframe isolation

widget.js draws a launcher button on your page and nothing else. The conversation runs in an iframe served from https://wiredesk.ai/w/<key>:
  • Same-origin chat traffic. Every chat request is made by the iframe to its own origin. Your page does not call the chat API, and the browser’s same-origin policy stops your page’s scripts, and anyone else’s, from reading the transcript inside the frame.
  • Checked postMessage both ways. widget.js only acts on messages from the WireDesk origin that come from its own frame. It posts to the frame with the WireDesk origin as the target. The panel only acts on messages from its parent window.
  • What your page learns. It receives only what the events carry: open and close, the text and role of messages the panel receives, and an unread count.
  • Late loading. The iframe is not created until a visitor hovers, focuses, touches or clicks the launcher, or your code opens it.

Rate limits and caps

Every public chat request counts against two limits at once. The send, poll, recent-chats and end requests all count. The IP limit is the one a script cannot get around by inventing a new visitor id for each request. Over either limit, the endpoint answers 429 with Retry-After: 60 and “You’re sending messages too quickly. Give it a moment.” If the limiter itself is unavailable, requests are allowed rather than failing chat for everyone, and the spend cap is the backstop. A single visitor message is capped at 2,000 characters.

API call actions

An API call action makes the agent send an HTTP request to a URL you configure. Because a model fills in the parameters from what a member of the public typed or said, the request is constrained on both sides. When you save the action:
  • The URL must be https.
  • The host must be a hostname, not an IPv4 or IPv6 address. localhost, hosts ending in .localhost, .local, .internal, .home.arpa or .onion, and single-label hosts are refused.
  • Every {{placeholder}} in the URL, headers or body must be a declared parameter. Parameter keys are short snake_case words, with at most 10 per action.
  • {{credential}} can go in a header or the body, never in the URL.
Each time it runs:
  • Only declared parameters are filled in. The model cannot add others.
  • Values are URL-encoded in the URL.
  • The URL is checked again with the same rules after the values are filled in, so a placeholder in the host cannot reach a private address.
  • Redirects are not followed. A 3xx response comes back as an error.
  • Timeouts: 5 seconds on a phone call, 15 seconds on chat and email.
  • The response is cut off at 1,500 characters on a call and 4,000 on chat and email.
The checks apply to the URL, not to the address its hostname resolves to. A hostname whose DNS points at a private address is not caught. Point actions only at hosts you control. Credentials:
  • Encrypted at rest with AES-256-GCM.
  • Only an admin or owner can add one.
  • After saving, they are only ever shown as their label and last four characters.
  • A credential is attached to the outgoing request, as a bearer token, a named header or a {{credential}} placeholder. It is never put in the model’s context. The agent sees only the response status and body.

Response headers

Every response from the app carries: Every page except the conversation frame also carries X-Frame-Options: DENY and Content-Security-Policy: frame-ancestors 'none', so the dashboard cannot be framed or clickjacked. The conversation frame at /w/<key> carries the frame-ancestors list described above, and no framing restriction if the widget has no list. Plain http requests are redirected to https with a 308, which keeps the method and body, and www.wiredesk.ai redirects to wiredesk.ai. The app does not send a full script-src Content-Security-Policy. widget.js is served with Cache-Control: public, max-age=300, stale-while-revalidate=3600, so a fix reaches embedded sites within minutes.