Install the Pong Chat widget
One line before </body> on every page. Your public key is pre-filled in the desk under Settings → Chat widget → Install.
The snippet
<script src="https://js.pong.ee/YOUR_PUBLIC_KEY" async></script>
That is all: an 8 KB loader, loaded async, that never blocks rendering. Your public key is part of the address.
Options on the same tag
Set the look and behaviour with data-* attributes. They win over PongSetup and the workspace defaults; yes/no attributes take "true" or "false".
<script src="https://js.pong.ee/YOUR_PUBLIC_KEY"
data-theme="auto" data-color="#16a34a" data-button-position="bl" async></script>
| Attribute | Values | Same as |
|---|---|---|
data-id | your public key, only with the older call.pong.ee/pong.js address | PongID |
data-language | en, ru | language |
data-theme | light | dark | auto | theme |
data-color | #hex | colors.buttonBg |
data-button-style | 3d | round | tab | buttonStyle |
data-button-position | br | bl | buttonPosition |
data-button-text | text | buttonText |
data-size | small | normal | large | size |
data-title, data-subtitle | text | title, subtitle |
data-chat-width, data-chat-height | pixels | chatWidth, chatHeight |
data-z-index | number | zIndex |
data-start-hidden | true | false | startHidden |
data-group-id | group id | groupId |
data-client-id | your user id | clientId |
data-deferred-loading | true | false | deferredLoading |
data-inject-to | element id | injectTo |
data-commands | true | false | commands |
data-data-layer | layer name, or false for none | dataLayer |
data-debug | true | false | debug |
data-onload | a function name, e.g. onPong or app.onPong | — (see below) |
Objects and functions — identity, the full colors set, callbacks — still go in PongSetup.
Run your code when Pong is ready
The simplest way to set things up: name your own function in data-onload. Once pong.js runs, it calls that function with the ready Pong API as its argument. The function can live anywhere on the page — if it is defined further down, it is called after the page has loaded. A dotted path such as app.onPong works too.
<script>
function onPong(Pong) {
Pong('on', 'message', function (m) { if (m.type === 'agent') Pong('openChat', true); });
}
</script>
<script src="https://js.pong.ee/YOUR_PUBLIC_KEY" data-onload="onPong" async></script>
There is no inline code on the loader tag, so this works under a strict Content Security Policy.
Calling Pong(...) before it loads
To call Pong('identify', …) or other methods from anywhere on the page before pong.js has loaded, add the queue line first. Calls made before the loader runs are kept and run once it does.
<script>window.Pong = window.Pong || function () { (window.Pong.q = window.Pong.q || []).push(arguments); };</script>
<script src="https://js.pong.ee/YOUR_PUBLIC_KEY" async></script>
The older snippet
Older forms keep working; you don’t need to change them: https://call.pong.ee/pong.js (or any address) with the key in data-id, and snippets that set PongID (or ChatraID) and PongSetup in a script above the loader.
Single-page apps
Add the snippet once, in the HTML shell. Pong follows history.pushState, replaceState and popstate on its own, so page views in React, Vue, Next.js or Nuxt routers reach the visitor's history in the desk without extra code. If your router changes the URL some other way, report the page yourself:
Pong('pageView');
React / Next.js
// app/layout.tsx (Next.js App Router)
import Script from 'next/script';
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
{children}
<Script src="https://js.pong.ee/YOUR_PUBLIC_KEY" strategy="afterInteractive" />
</body>
</html>
);
}
Website builders
| Platform | Where to paste |
|---|---|
| WordPress | A header/footer code plugin (e.g. WPCode) → Footer, or your theme’s footer.php before wp_footer() |
| Shopify | Online Store → Themes → Edit code → theme.liquid, before </body> |
| Webflow | Site settings → Custom code → Footer code |
| Wix | Settings → Custom code → Add code, place in Body – end, all pages |
| Squarespace | Settings → Advanced → Code injection → Footer |
| Tilda | Site settings → More → HTML code for the HEAD / end of BODY |
| Framer | Site settings → General → Custom code → End of <body> tag |
Google Tag Manager
Tags → New → Custom HTML, paste the same line, trigger All Pages, then Submit → Publish.
<script src="https://js.pong.ee/YOUR_PUBLIC_KEY" async></script>
To set PongSetup, PongIntegration or call Pong('identify', …) from the tag, put them above the loader in the same tag, after the queue line. Chat events reach your GTM triggers by themselves — see Events in Google Tag Manager.
Moving from Chatra
Pong’s loader understands Chatra’s API, so your existing code keeps working. In your current Chatra snippet, change one thing — the script address:
https://call.chatra.io/chatra.js → https://js.pong.ee/YOUR_PUBLIC_KEY
The key in the address wins over ChatraID, so that line can stay or go.
Everything else can stay: Chatra(...) calls (including ones made before the script loads), ChatraSetup, ChatraGroupID and ChatraIntegration are read as their Pong twins (Pong(...), PongSetup, PongGroupID, PongIntegration). window.Chatra points to Pong unless Chatra’s own widget is still on the page. When convenient, rename to the Pong names — nothing breaks either way.
Load later
To keep the first paint of a heavy page clean, ask the loader to wait for the load event and an idle moment:
<script src="https://js.pong.ee/YOUR_PUBLIC_KEY" data-deferred-loading="true" async></script>
Content Security Policy
If your site sends a CSP header, allow the loader and the chat frame:
script-src https://js.pong.ee https://chat.pong.ee https://call.pong.ee;
frame-src https://chat.pong.ee;
Check it works
- Open your site — the chat button appears in the bottom-right corner.
- Send a message from the widget.
- It arrives in the desk at app.pong.ee within a second.
data-debug="true" on the tag the loader logs unsupported calls and debug output there.