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: *andCache-Control: public, max-age=60. Once a list exists, it echoes the caller’s own origin, addsVary: Originand switches toCache-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:*andhttp://*.domain:*, so a developer’s local http build still works. NoX-Frame-Optionsis 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 noOriginheader. They are accepted only when the browser marks themSec-Fetch-Site: same-origin, or, on browsers that do not send that header, when theRefereris WireDesk’s own origin. A caller with neither header is refused.
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 gets404. 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
- Your page gives
widget.jsthe identity, throughwindow.wiredeskSettings.userorwiredesk("identify", …). widget.jspasses it to the panel in the iframe URL’s fragment (#wd=…), or bypostMessageif 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 andRefererheaders.- The panel sends it to the chat endpoints in an
X-Wd-Identityrequest header, as percent-encoded JSON.
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
postMessageboth ways.widget.jsonly 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.arpaor.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.
- 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
3xxresponse 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.
- 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.