Security and CSP
Allowed domains, test conversations, what protects what, Content Security Policy, pinned installs with SRI, and privacy.
Allowed domains
Each widget has a list of domains it may run on, in its Security tab at Settings → Widgets.
- Before you add any domain, the widget works on any
https://site. Ticketping records each site it loads on and lists it under Seen recently. - Add the domains you expect. Use an exact host like
app.acme.com, or a wildcard like*.acme.com. A wildcard matches subdomains, notacme.comitself. - Turn on Only allow these domains before you go live. Other sites then get
origin_not_allowed, and still show up under Seen recently so you notice a domain you forgot.
Only https:// sites are accepted, apart from localhost.
Localhost and test conversations
localhost, 127.0.0.1 and [::1] always work, over http or https, on any port, so the snippet works locally with no setup. You can turn this off with Allow localhost.
Conversations from localhost are test conversations:
- They’re tagged TEST in the dashboard and
[TEST]in Slack, and routed like real ones. - They’re left out of reports.
- They’re deleted after 30 days.
- They only show up in the widget on localhost and test domains, so your real users never see them in their history.
Mark a domain as Test (for example staging.acme.com) to get the same treatment there.
What protects what
| Concern | Protection |
|---|---|
| Someone pretending to be one of your users | Verified identity: a token signed on your server with your identity secret. Turn on Require verified identity to refuse unsigned identify calls |
| Reading another user’s conversations | History across devices needs a verified identity. Anonymous conversations are tied to a random token stored in that browser |
| Copying your snippet onto another site | Allowed domains. The publishable key is public, so the domain list is what limits where it works |
| A stolen identity token | Each token is single use and expires within 10 minutes |
| A leaked identity secret | Rotate it, then revoke the old one. Revoking takes effect at once |
| Turning on features from the browser console | Features turned off in the dashboard are enforced by the server |
| Malicious content in messages | Agent and email messages are sanitized on the server and again in the widget. Uploaded files other than images are always downloaded, never shown inline |
| Spam | Rate limits per visitor and per IP |
Allowed domains are checked with the browser’s Origin header. Scripts outside a browser can fake it, so treat the domain list as one layer, not the thing that protects your data. Verified identity is.
Content Security Policy
If your site sends a Content-Security-Policy header, add these sources:
| Directive | Sources | Why |
|---|---|---|
script-src | https://widget.ticketping.com | The loader and the widget |
connect-src | https://api.ticketping.com wss://api.ticketping.com https://widget.ticketping.com | The API, the live connection, and emoji data |
img-src | https://media.giphy.com https://*.giphy.com blob: data: plus your attachment host (below) | GIFs, avatars, image previews before upload |
media-src | https://media.giphy.com https://*.giphy.com | GIFs play as muted videos |
style-src | 'unsafe-inline' | The widget’s styles (below) |
For example:
Content-Security-Policy:
script-src 'self' https://widget.ticketping.com;
connect-src 'self' https://api.ticketping.com wss://api.ticketping.com https://widget.ticketping.com;
img-src 'self' https: blob: data:;
media-src 'self' https://media.giphy.com https://*.giphy.com;
style-src 'self' 'unsafe-inline'If you turn off GIFs for the widget, you can drop the GIPHY sources.
Images in conversations. Attachments and avatars load from signed file-storage URLs whose host isn’t fixed yet. Until it is, img-src https: is the reliable choice. If your policy can’t allow that, attached images and some avatars won’t show; the conversation still works.
Styles. The widget renders inside a shadow root, so your page’s CSS doesn’t affect it and its CSS doesn’t leak into your page. Its styles are a <style> element inside that shadow root, which CSP treats like any inline style, so style-src needs 'unsafe-inline'. Nonces aren’t supported yet. The widget adds nothing to your page’s <head> apart from its script.
Pinned install with Subresource Integrity
The loader at /v2/loader.js always loads the latest 2.x release. If your policy requires Subresource Integrity, or you want to upgrade on your own schedule, load a specific version directly:
<script>
window.Ticketping ||= (...args) => (Ticketping.q ||= []).push(args)
Ticketping('init', { publishableKey: 'pk_...' })
</script>
<script
src="https://widget.ticketping.com/v2/2.0.0/widget.js"
integrity="sha384-..."
crossorigin="anonymous"
async
></script>- Replace
2.0.0with the version you want. Versioned files never change. - Get the hash from
https://widget.ticketping.com/v2/2.0.0/manifest.json. Itsintegrityobject has asha384-...hash forwidget.jsand every other file in that version. - Without the loader there’s no
data-key, so callinityourself, as above. - The pinned bundle loads straight away instead of waiting for the browser to be idle.
- Features that load on demand, like the emoji picker, come from the same versioned folder.
You’ll need to update the version and hash to get fixes. New releases are listed on npm; the CDN uses the same version numbers.
Privacy
What the widget stores in the browser
The widget uses localStorage, never cookies. It stores a random visitor token and, for identified users, a session. Keys start with tp: and your publishable key. logout and consent('denied') delete them.
Consent mode
If you need consent before storing anything, start the widget in pending mode and grant consent from your cookie banner:
Ticketping('init', { publishableKey: 'pk_...', consent: 'pending' })
// When the visitor accepts:
Ticketping('consent', 'granted')While pending, the widget stores nothing and doesn’t contact Ticketping’s API. Ticketping('consent', 'denied') stops it and clears what it stored.
GIPHY
GIFs load straight from GIPHY’s servers in the visitor’s browser, so GIPHY sees the visitor’s IP address and browser details. GIF searches go through Ticketping’s servers, so GIPHY doesn’t see who searched. GIFs are off by default for new widgets. Mention GIPHY in your privacy policy if you turn them on. See GIFs and emoji.
What Ticketping keeps, and for how long
| Data | Kept |
|---|---|
| Conversations that became tickets | With the ticket, until you delete it |
| Anonymous conversations that never became tickets (for example, AI-only chats) | 12 months after the last activity |
| Anonymous visitors | 12 months after the last activity, along with their remaining non-ticket conversations |
| Test conversations (localhost and test domains) | 30 days after they started, including any ticket created from them |
| Ended sessions | 30 days |
| Uploads never sent in a message | 24 hours |
| Visitor details | The IP address isn’t stored on the visitor. The user agent is reduced to browser, OS and device type. Page context is kept with the conversation, under the same rules |
Conversations are only created when the visitor sends a message, so opening the widget doesn’t create any data.
Copy prompt for your AI coding agent
Make this site's Content-Security-Policy work with the Ticketping chat widget (v2).
Docs index: https://ticketping.com/llms.txt
This page as Markdown: https://ticketping.com/docs/security.md
1. Find where this project sets its CSP (headers config, middleware, meta tag or framework option).
2. Add: script-src https://widget.ticketping.com; connect-src https://api.ticketping.com wss://api.ticketping.com https://widget.ticketping.com;
img-src https: blob: data: (or at least https://media.giphy.com https://*.giphy.com); media-src https://media.giphy.com https://*.giphy.com;
style-src 'unsafe-inline'. Keep the existing sources.
3. If the site needs consent before storage, start the widget with consent: 'pending' and call Ticketping('consent', 'granted')
from the existing cookie banner's accept handler.