Everything the widget can do
Install it, configure it, drive it from your own code, or talk to the API directly. Written against the shipping source — if something here disagrees with the product, the documentation is the bug.
Quick start
One script tag, one Telegram connect. Two minutes, no build step, no server of your own.
- 1Create an assistant at qiksy-assist.qiksy.workers.dev/start. You get a tenant key (
qa_…), a ready snippet and a private auto-login link to your panel — save that link before you leave the page. It is shown once and is never emailed to you. - 2Paste the snippet before
</body>on your site. The bubble appears immediately. - 3Open your settings panel and press Connect Telegram. It deep-links the bot assigned to you and binds the chat in one tap — messages from the site then arrive there. (Always take that link from the panel: assistants are spread over several bots, so the right one is the one your panel names.)
- 4Reply with a normal swipe-reply. Your answer appears in the visitor’s widget within seconds.
<script src="https://qiksy-assist.qiksy.workers.dev/widget.js?key=qa_yourkey" defer></script>Visitors stay anonymous: no account, no login, no tracking cookie. A session id lives in their localStorage for 30 days so the conversation survives a reload.
Installing on your site
The widget is one self-contained file with no dependencies. Anywhere you can add a script tag, it works.
Plain HTML
Paste the snippet before the closing </body> tag. defer keeps it off the critical path — the widget never blocks your page render.
WordPress
Appearance → Theme File Editor → footer.php, before </body>. Or use any “insert header and footer” plugin and put the snippet in the footer field — no theme edit, survives theme updates.
Tilda
Site Settings → More → HTML code for the BODY section. Paste, save, republish the site (Tilda only ships the change on republish).
Shopify
Online Store → Themes → Edit code → layout/theme.liquid, before </body>.
Webflow
Project Settings → Custom Code → Footer Code. Publish to apply — custom code does not run in the Designer preview.
Next.js / React
Render it as a real script tag, not an import. In the App Router, put it in app/layout.tsx:
import Script from 'next/script'
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Script
src="https://qiksy-assist.qiksy.workers.dev/widget.js?key=qa_yourkey"
strategy="lazyOnload"
/>
</body>
</html>
)
}window.__qiksyAssist, so a second injection is a no-op, but a removed-and-re-added script loses the open conversation panel.Self-hosting the file
You may host widget.js yourself instead of loading it from our worker. Then the endpoint is no longer implied and you pass it explicitly:
<script src="/js/widget.js" defer
data-endpoint="https://qiksy-assist.qiksy.workers.dev"
data-key="qa_yourkey"></script>You then own updates: fixes and new features ship to the worker-served copy automatically, to your copy only when you replace the file.
Connecting Telegram
Telegram is your side of the conversation. Visitors never see it, never need it, and never learn your number.
Connecting
Open the deep-link in your settings panel — it is t.me/<your bot>?start=<your key> — and press Start. The chat is bound to your assistant and site messages begin arriving there.
Replying
- Swipe-reply to a message — the standard Telegram reply gesture. That is the whole workflow.
/r CODE text— reply by the four-character conversation code shown in the ping (💬 #A7K2), useful when the original message has scrolled away./admin— the bot re-sends your auto-login panel link. It is your keychain; do not forward that message.
The bot reacts 👍 when your reply has been routed to the visitor. No reaction means it could not tell which conversation you meant — reply directly to the message, or use the code form.
Files
Both directions. Visitors may send PNG, JPEG, WebP or PDF up to 10 MB; you reply with a photo or document as a reply. Files are relayed through Telegram, which is also where they are stored — nothing is kept on our side beyond the file reference. Because Telegram is the store, uploads only work once your Telegram is connected — before that a visitor trying to attach a file is told so. Text messages, meanwhile, are stored and replayed to you on first connect.
Which bot serves you
Telegram caps one bot at roughly 30 messages per second across all of its chats, so assistants are distributed over a pool of bots. Yours is assigned when you sign up, and the deep-link in your panel always points at the right one — you never pick.
On the Pro plan you may connect your own bot instead: paste its @BotFather token in the panel and the assistant runs under your brand, with a delivery ceiling nobody else shares. An already-connected own bot keeps working on any plan.
The settings panel
Everything visible in the widget can be changed without touching your site again.
Your panel lives at /admin?key=… on the worker. The auto-login link is shown once, on the page where you create the assistant — bookmark it there and then. The token rides in the URL fragment, so it never reaches the server as a query parameter. Once Telegram is connected you can always have the link re-sent by messaging /admin to the bot — but that needs a binding to already exist, which is why the link matters before you connect anything.
- Telegram — connect, test, disconnect; a no-terminal wizard including your own bot token.
- Appearance — brand name, accent colour, theme, position, logo (upload with circle-crop, or a URL).
- Texts — greeting, tagline, teaser bubble, the menu of request types.
- Channels — which of chat / request / subscribe / WhatsApp / video / voice are offered.
- Inbox — every lead and conversation stored on our side, readable even with Telegram completely down.
- Live preview — a real widget in a drawer that re-renders on every field edit.
Owner mode
Open your own site with #qas-owner appended to the URL. That browser is remembered as yours, and service notices (Telegram not connected, trial ending) are shown only to you. Visitors never see them.
Configuration reference
Every option can be set as a data-attribute on the script tag, or on a window.QiksyAssist object before the script loads.
Precedence
Explicit data-* or window.QiksyAssist → remote config from the panel → built-in default. An option you hard-code on the page can never be overridden from the panel; that is deliberate, so a deploy cannot be undone by a settings change.
Connection
| Attribute | Default | What it does |
|---|---|---|
| data-key | — | Tenant key (qa_…). Pre-wired when the widget is loaded from the worker with ?key=. |
| data-endpoint | — | Worker base URL, or the full /lead URL. Only needed when self-hosting the file. |
| data-email | — | Fallback mailbox. With no endpoint, the request flow degrades to the visitor’s mail app instead of breaking. |
Appearance
| Attribute | Default | What it does |
|---|---|---|
| data-brand | Assistant | Name shown in the widget header. |
| data-accent | #EAB308 | One hex colour. The entire palette — gradients, glows, contrast ink — is derived from it. |
| data-theme | auto | auto | dark | light. Auto follows the visitor’s system setting. |
| data-position | right | right | left corner. |
| data-offsetx | 18 | Pixels from the chosen side. |
| data-offsety | 18 | Pixels from the bottom. |
| data-zindex | 2147482000 | Stacking order. Raise it only if something on your site still covers the bubble. |
| data-logo | — | Empty = the default mark · none = no mark · an image URL or data URI. Pro — on a lower plan the custom image is ignored and the default mark shows. |
| data-branding | on | off hides the “⚡ Qiksy Assist” note. Requires a paid plan. |
| data-mobile | on | off hides the widget below 640px. |
Language and copy
| Attribute | Default | What it does |
|---|---|---|
| data-lang | auto | auto | en | ru | uk. Auto picks by visitor region. |
| data-greeting | — | First line the assistant says. |
| data-tagline | — | Reply-time line under the brand. Keep it honest — it sets the expectation you are judged against. |
| data-teaser | — | The small bubble that appears next to the launcher before anyone clicks. |
| data-intents | — | Comma-separated menu of request types, e.g. Pricing, Book a call, Support. |
| data-budgets | — | Comma-separated budget buckets. Any currency, any market. |
Channels
| Attribute | Default | What it does |
|---|---|---|
| data-modes | chat,order,subscribe | Which entries the menu offers: chat, order, subscribe, email. Any combination, any single one. WhatsApp, Telegram and video are not set here — each switches itself on when its own option is set. |
| data-whatsapp | — | Digits-only number (8–15). Adds a WhatsApp hand-off that carries the conversation context. |
| data-telegram | — | @username or t.me link. Adds a direct-message button. Paid channel — hidden on Free, exactly like WhatsApp. |
| data-video | off | on offers a browser video room; the link is delivered as a lead. |
| data-mic | on | off hides voice dictation. Rendered only where the browser supports it. |
| data-budget | on | off removes the budget step from the request flow. |
data-modes ends up empty the widget falls back to the request flow, so a visitor always has a way to reach you. Voice is an input method inside chat, not a menu entry of its own.Known visitors
| Attribute | Default | What it does |
|---|---|---|
| data-name | — | Name of a logged-in visitor. |
| data-contact | — | Their email or phone. Present = the contact step is skipped. |
| data-user-meta | — | Free text you want to see next to them, e.g. Plan Pro · client since 2024 · ID 8842. |
For values known only at runtime use identify() instead — see the JavaScript API.
Object form
Set window.QiksyAssist before the script loads. The keys are the attribute names minus the data- prefix — including the hyphen, so it is 'user-meta', not userMeta. A camelCase key is silently ignored. Attributes on the script tag still win over this object.
<script>
window.QiksyAssist = {
key: 'qa_yourkey',
brand: 'Northwind',
accent: '#2563EB',
intents: ['Pricing', 'Book a demo', 'Support'],
'user-meta': 'Plan Pro · ID 8842'
}
</script>
<script src="https://qiksy-assist.qiksy.workers.dev/widget.js" defer></script>JavaScript API
The widget exposes exactly one global. Everything else stays inside its shadow root.
window.__qiksyAssist = {
version: '0.2.0',
open(), // open the panel
close(), // close it
toggle(), // flip it
ask({ intent, context }), // one-step lead from any CTA
identify({ name, contact, meta }) // seed a known visitor
}window.__qiksyAssist?.open()) or run them from a user gesture, by which time it always exists.Turning any button into a lead
Add data-qiksy-ask to any element on your page. Clicking it opens the widget into a one-step contact flow and sends a lead carrying that intent, the page and any context you attach — no form, no separate landing page.
<button data-qiksy-ask="Request a quote"
data-qiksy-context="Plan: Business · Seats: 25">
Get a quote
</button>For context computed at runtime, call it in JavaScript:
document.querySelector('#quote').addEventListener('click', () => {
window.__qiksyAssist.ask({
intent: 'Request a quote',
context: 'Cart: ' + cart.total + ' · Items: ' + cart.count
})
})Identifying a logged-in visitor
Call identify() once you know who the visitor is. You then see a real person in Telegram instead of an anonymous code. A known contact also skips the contact question in live chat — the request and subscribe flows still ask, because there the contact is the point of the form.
window.__qiksyAssist.identify({
name: 'Anna Kovalenko',
contact: '[email protected]',
meta: 'Plan Pro · client since 2024 · ID 8842'
})One more global worth knowing: ask({context}) writes window.__qiksyLeadContext on your page and never clears it, so every later lead from that page carries the same context until something overwrites it. Set it fresh per call if that matters. meta is free text up to 300 characters and appears as 👤 … on the first ping of every conversation. Send what makes the reply better — plan, order number, lifetime value — and nothing you would not want in a Telegram message.
Delivery channels
Where messages and leads go, and what happens when a channel is down.
- Telegram — for a hosted assistant, the delivery channel. Two-way: pings out, swipe-replies back.
- Email and webhook mirroring — self-hosted deployments only. They are configured deployment-wide (
RESEND_API_KEY+LEAD_EMAIL_TO/LEAD_EMAIL_FROM,FORWARD_WEBHOOK) and are deliberately switched off for hosted tenants, so one customer’s leads can never reach another’s mailbox. - The panel inbox — every lead and conversation is stored and readable in your panel regardless of channel, which is what makes a Telegram outage survivable.
Nothing is lost when a channel fails
Four independent layers, because a visitor must never see an error caused by our outage:
- 1Every lead is stored before any delivery is attempted, and the visitor gets a success response even if every channel fails.
- 2Telegram calls retry three times with backoff, honouring rate-limit hints.
- 3Undelivered pings and leads are re-sent automatically on the next successful delivery, and on first Telegram connect — up to 72 hours back.
- 4The panel inbox reads straight from storage, so you can work through everything with Telegram fully down.
HTTP API
The contract between the widget and the worker. You need this only for a custom client or a server-side integration.
Base URL: https://qiksy-assist.qiksy.workers.dev. Every route takes ?key=<tenant key>. CORS is open; the endpoints below are the public surface.
| Method | Route | Purpose |
|---|---|---|
| POST | /lead | Submit a lead. Fans out to every configured channel. |
| POST | /chat | Send a chat message (JSON, or multipart with a file). |
| GET | /chat | Poll for owner replies: ?session=&after=. |
| GET | /config | Public widget config for this tenant. Cached 60s. |
| GET | /file | Session-scoped attachment proxy: ?session=&m=. |
| GET | /health | Returns ok. For uptime checks. |
Lead payload (v1)
{
"v": 1,
"lang": "en",
"page": "https://yoursite.com/pricing",
"intent": "Request a quote",
"brief": "Need 25 seats, starting in March",
"budget": "$1k–5k",
"contact": "[email protected]",
"name": "Anna Kovalenko",
"context": { "summary": "Plan: Business", "link": "https://yoursite.com/pricing" },
"dwellMs": 9400,
"hp": ""
}context must be an object ({summary, link}) — a plain string is accepted by the request and then silently dropped. hp is a honeypot and must stay empty. dwellMs is how long the visitor spent before submitting; anything under 2.5 seconds is treated as automated. Both cases return a normal 200 and deliver nothing — bots are not told they were caught.
Chat protocol (v1.1)
POST /chat
{
"v": 1,
"type": "msg",
"session": "qs_a1b2c3d4",
"text": "Do you ship to Poland?",
"lang": "en",
"page": "https://yoursite.com/",
"dwellMs": 5200
}
→ { "ok": true, "id": 12, "code": "A7K2" }
GET /chat?session=qs_a1b2c3d4&after=12
→ { "ok": true, "messages": [ { "id": 13, "text": "Yes — 2–4 days.", "ts": 1785… } ] }The session id is generated by the client and must match qs_[a-z0-9]{8,32}. GET /chat returns owner messages only, newer than after. Polling is not just etiquette — it is enforced (see the limits below), so match the widget’s cadence: 3.5s with the panel open, backing off to 60s idle, paused entirely on a hidden tab.
GET /chat is metered too — 120/minute and 2000/hour per IP. Exceeding any of them returns 429 with {ok:false, retry:true}. Behind one office NAT these are shared by everyone on that address.Versioning
The v field is the contract version. Breaking changes bump it and the previous version keeps being accepted — an embed that works today is not broken by a later release.
Privacy and security
What is stored, where it lives, and what the widget will never do to your page.
What is stored
- Conversations and leads for your assistant, in an isolated database of your own. Every tenant is a separate storage object — not rows in a shared table.
- On the visitor’s device: a session id and their own message history in
localStorage, expiring after 30 days. - Attachments are relayed to Telegram, which is also the storage. We keep the reference, not the file.
No visitor accounts, no cross-site tracking, no advertising identifiers, no third-party analytics inside the widget.
What it will not do to your page
- It renders inside a shadow root, so your CSS cannot break it and it cannot leak styles into your page. Every class is prefixed
qas-. - It defines exactly one global,
window.__qiksyAssist. - It never opens itself. No auto-open, no exit-intent pop-up.
- It asks for contact details only after the first message, and the visitor can skip it.
Content Security Policy
If your site sets a CSP, allow the worker as a script and connect source. Two more directives matter: the launcher mark and the brand bolt are inlined as data: images, so img-src must allow data:; and the widget injects one stylesheet, copying your page’s nonce onto it — so a nonce-based policy needs no unsafe-inline, but the style element does have to be permitted:
script-src 'self' https://qiksy-assist.qiksy.workers.dev;
connect-src 'self' https://qiksy-assist.qiksy.workers.dev;
img-src 'self' data:;
style-src 'self' 'nonce-<your-nonce>';Spam protection
A honeypot field, a dwell-time gate and per-IP and per-session rate limits, all before anything reaches your Telegram. A submission that fails the dwell-time gate is kept and shown in the panel inbox as suspect rather than pinged to your phone. A filled honeypot — a field no human can see — is dropped entirely and stored nowhere: only automated traffic ever fills it. Both get a normal success response, so a bot is never told it was caught.
Plans and limits
Chat is free forever. Paid plans remove the badge and add premium channels.
| Free | Start | Pro | |
|---|---|---|---|
| Two-way Telegram chat | Unlimited | Unlimited | Unlimited |
| Leads, files, anti-spam | Yes | Yes | Yes |
| Panel inbox (readable with Telegram down) | Yes | Yes | Yes |
| “⚡ Qiksy Assist” badge | Shown | Removed | Removed |
| WhatsApp button, Telegram button, video call, voice input | — | Yes | Yes |
| Custom logo in the widget | — | — | Yes |
| Your own Telegram bot | — | — | Yes |
New assistants get a 14-day Start trial — the badge removal and the premium channels, not the Pro extras. When it ends, chat keeps working: it never locks. Only the premium extras switch off, and a 7-day grace period runs before even that.
Conversation limits
There are none. No cap on conversations, no per-seat pricing, no “active contacts” meter. The rate limits described in the HTTP API are anti-abuse floors, not a plan quota — normal traffic never reaches them.
Troubleshooting
The bubble does not appear
- Check the browser console for
[qiksy-assist]warnings — the widget says out loud when it has nowhere to send messages. - Confirm the script tag is in the page source (view-source, not the inspector) — some builders strip unknown tags on publish.
- On Tilda and Webflow the change only ships on republish.
- If your site sets a CSP, see the policy snippet under Privacy and security.
- Below 640px the widget is hidden when
data-mobile="off".
Messages do not reach Telegram
- Open the panel and press Test in the Telegram card — it sends a message right then and shows whether it went through. It reports success or failure, not the reason, so if it fails work through the two causes below.
- If you never pressed Start in the bot, the binding does not exist yet. Nothing is lost: connect now and the backlog is delivered.
- If you changed Telegram accounts, disconnect in the panel first — a binding cannot be silently reassigned.
My reply did not reach the visitor
Check for the 👍 reaction on your message. No reaction means the reply was not attributed to a conversation — use a real swipe-reply, or /r CODE text with the code from the ping.
The widget looks wrong on my site
It cannot inherit your CSS — it lives in a shadow root. What it does inherit is stacking: if something covers it, raise data-zindex. If it overlaps a fixed footer or a cookie bar, move it with data-offsety.
Still stuck
Use the widget on this page — it goes to the same place your customers’ messages go, and a person answers it.