diff --git a/apps/docs/docs.json b/apps/docs/docs.json
index e756f1c15e..822dd2fc84 100644
--- a/apps/docs/docs.json
+++ b/apps/docs/docs.json
@@ -221,7 +221,12 @@
},
{
"group": "Resources",
- "pages": ["guides/general/accessibility", "guides/general/security", "resources/license"]
+ "pages": [
+ "guides/general/accessibility",
+ "resources/telemetry",
+ "guides/general/security",
+ "resources/license"
+ ]
}
]
},
diff --git a/apps/docs/resources/telemetry.mdx b/apps/docs/resources/telemetry.mdx
new file mode 100644
index 0000000000..80ffdb7b45
--- /dev/null
+++ b/apps/docs/resources/telemetry.mdx
@@ -0,0 +1,244 @@
+---
+title: Telemetry
+sidebarTitle: Telemetry
+keywords: "telemetry, analytics, usage tracking, billing, license key, document tracking"
+---
+
+SuperDoc collects lightweight telemetry to track document opens for usage-based billing. Telemetry is enabled by default and runs silently in the background — it never blocks rendering or breaks your app.
+
+## What gets collected
+
+Each time a document is opened or imported, SuperDoc sends a single event containing:
+
+| Field | Description | Example |
+|-------|-------------|---------|
+| `documentId` | A hashed document identifier (not the content itself) | `a1b2c3d4...` |
+| `documentCreatedAt` | The document's original creation timestamp | `2024-01-15T10:30:00Z` |
+| `superdocVersion` | The version of the SuperDoc library | `1.15.0` |
+| `browserInfo` | User agent, hostname, and screen size | `{ hostname: "app.example.com", ... }` |
+| `metadata` | Custom key-value pairs you optionally provide | `{ customerId: "123" }` |
+
+SuperDoc does **not** collect:
+- Document content or text
+- User identities or personal data
+- Keystrokes, edits, or change history
+- Cookies or session tokens (`credentials: 'omit'`)
+
+## How it works
+
+1. You open or import a document
+2. SuperDoc generates a **document identifier** — a hash derived from the file's metadata (not its content)
+3. A single `POST` request fires to the telemetry endpoint
+4. If the request fails (network error, blocked by firewall), it fails silently
+
+```
+Document opened → Generate identifier hash → POST to endpoint → Done
+```
+
+
+The telemetry request is non-blocking and fire-and-forget. Your editor loads regardless of the outcome.
+
+
+## Configuration
+
+Pass `licenseKey` and `telemetry` in your SuperDoc or SuperEditor config:
+
+
+
+```javascript SuperDoc
+import { SuperDoc } from 'superdoc';
+
+const superdoc = new SuperDoc({
+ selector: '#editor',
+ document: yourFile,
+ licenseKey: 'your-license-key',
+ telemetry: {
+ enabled: true,
+ },
+});
+```
+
+```javascript SuperEditor
+import { Editor } from 'superdoc/super-editor';
+
+const editor = await Editor.open(file, {
+ element: document.querySelector('#editor'),
+ licenseKey: 'your-license-key',
+ telemetry: {
+ enabled: true,
+ },
+});
+```
+
+
+
+### Options
+
+
+ Your organization's license key. Links document opens to your account for billing.
+ Defaults to `community-and-eval-agplv3` if not provided.
+
+
+
+ Enable or disable telemetry. Default: `true`.
+
+
+
+ Override the telemetry endpoint. Default: `https://ingest.superdoc.dev/v1/collect`. Useful for proxying through your own infrastructure.
+
+
+
+ Custom key-value pairs included with every event. Use this to attach your own identifiers (customer ID, environment, etc.).
+
+
+### Disabling telemetry
+
+Set `enabled: false` to turn telemetry off entirely:
+
+```javascript
+const superdoc = new SuperDoc({
+ selector: '#editor',
+ document: yourFile,
+ telemetry: {
+ enabled: false,
+ },
+});
+```
+
+No network requests will be made. Document metadata (GUID and timestamp) is still generated locally so files export correctly.
+
+## License key
+
+The license key identifies your organization. It's sent as an `X-License-Key` header with every telemetry request.
+
+| License type | Key | How to get it |
+|-------------|-----|---------------|
+| Community / evaluation | `community-and-eval-agplv3` (default) | No action needed — used automatically |
+| Commercial | Your organization key | Email [q@superdoc.dev](mailto:q@superdoc.dev) |
+
+If you're evaluating SuperDoc or using it under AGPLv3, you don't need to configure a license key. The community key is applied automatically.
+
+For a commercial license key, email [q@superdoc.dev](mailto:q@superdoc.dev). You'll receive a key tied to your organization that unlocks usage tracking for your account.
+
+## How document uniqueness works
+
+SuperDoc needs to identify each document uniquely so that opening the same file twice counts as one document, not two. This matters for accurate billing.
+
+### Identifier strategies
+
+SuperDoc uses two strategies depending on what metadata the file contains:
+
+**1. Metadata hash — file already has a GUID and timestamp**
+
+DOCX files created by Microsoft Word (and other tools) typically contain a unique GUID and a creation timestamp in their internal metadata (`docProps/` and `word/settings.xml`). When both are present, SuperDoc hashes them together:
+
+```
+documentId = hash(GUID | creationTimestamp) → "HASH-A1B2C3D4"
+```
+
+This is the most stable strategy. The same file always produces the same identifier regardless of content edits, because the hash is derived from metadata, not content.
+
+**2. Generated identifier — file is missing metadata**
+
+Some tools produce DOCX files without a GUID or timestamp. When either is missing, SuperDoc does two things:
+
+1. **For the current open**: generates a content hash of the raw file bytes so the same file still produces a consistent identifier
+2. **For future opens**: generates the missing GUID and/or timestamp and embeds them into the document's metadata
+
+The generated metadata is saved when you export. On the next import, the file now has both a GUID and timestamp, so SuperDoc switches to the stable metadata hash automatically.
+
+```
+First open: no GUID in file → content hash used, GUID + timestamp generated
+Export: GUID + timestamp written into the file
+Next open: GUID + timestamp found → metadata hash (stable from now on)
+```
+
+This self-healing behavior means document identification improves automatically as files pass through SuperDoc. After one export cycle, every document gets a permanent, edit-resistant identifier.
+
+## Payload example
+
+Here's what a telemetry request looks like on the wire:
+
+```
+POST https://ingest.superdoc.dev/v1/collect
+Content-Type: application/json
+X-License-Key: your-license-key
+```
+
+```json
+{
+ "superdocVersion": "1.15.0",
+ "browserInfo": {
+ "userAgent": "Mozilla/5.0...",
+ "currentUrl": "https://app.example.com/doc/123",
+ "hostname": "app.example.com",
+ "screenSize": { "width": 1920, "height": 1080 }
+ },
+ "metadata": {
+ "customerId": "cust-456"
+ },
+ "events": [
+ {
+ "timestamp": "2026-02-12T14:30:00.000Z",
+ "documentId": "a1b2c3d4e5f6...",
+ "documentCreatedAt": "2024-01-15T10:30:00Z"
+ }
+ ]
+}
+```
+
+## Try it: document counting demo
+
+See document uniqueness in action. Upload a DOCX file and watch the counter — then try the steps below.
+
+import { DocCounter } from '/snippets/components/doc-counter.jsx'
+
+
+
+
+
+ Click **Upload DOCX** and pick any `.docx` file. The counter goes to **1**. SuperDoc reads the file's internal metadata and generates a document identifier.
+
+
+ Make some changes in the editor — add text, delete a paragraph, change formatting. The identifier stays the same because it's based on metadata, not content.
+
+
+ Click **Export & re-import**. SuperDoc exports the document to DOCX and immediately re-imports it. The counter stays at **1** — same metadata, same identifier.
+
+
+ Upload a second DOCX file. The counter goes to **2**. Each file with distinct metadata gets its own identifier.
+
+
+ Upload the original file again. The counter stays at **2**. SuperDoc recognizes it as the same document.
+
+
+
+
+The event log at the bottom shows exactly what SuperDoc sees: the identifier hash, whether a document is new or recognized, and how many times each has been opened.
+
+
+## FAQ
+
+
+
+
+No. The request is non-blocking and fire-and-forget. Your editor loads and renders regardless of the telemetry outcome.
+
+
+
+Nothing. Errors are caught silently. No retries, no queuing, no user-visible errors. Your app continues to work normally.
+
+
+
+Yes. Set `telemetry.endpoint` to your proxy URL. The payload format stays the same.
+
+
+
+Not if it has metadata. SuperDoc hashes the document's GUID and creation timestamp, so the same file produces the same identifier every time. See [How document uniqueness works](#how-document-uniqueness-works) above.
+
+
+
+No. The community key (`community-and-eval-agplv3`) is applied automatically when you don't provide one.
+
+
+
diff --git a/apps/docs/snippets/components/doc-counter.jsx b/apps/docs/snippets/components/doc-counter.jsx
new file mode 100644
index 0000000000..d2c7284edb
--- /dev/null
+++ b/apps/docs/snippets/components/doc-counter.jsx
@@ -0,0 +1,263 @@
+export const DocCounter = ({ height = '350px' }) => {
+ const [documents, setDocuments] = useState(new Map());
+ const [currentDoc, setCurrentDoc] = useState(null);
+ const [ready, setReady] = useState(false);
+ const [log, setLog] = useState([]);
+ const superdocRef = useRef(null);
+ const containerIdRef = useRef(`editor-${Math.random().toString(36).substr(2, 9)}`);
+
+ const addLog = (message) => {
+ setLog((prev) => [...prev, { time: new Date().toLocaleTimeString(), message }]);
+ };
+
+ useEffect(() => {
+ const link = document.createElement('link');
+ link.rel = 'stylesheet';
+ link.href = 'https://unpkg.com/superdoc@latest/dist/style.css';
+ document.head.appendChild(link);
+
+ const script = document.createElement('script');
+ script.src = 'https://unpkg.com/superdoc@latest/dist/superdoc.umd.js';
+ script.onload = () => setTimeout(() => initializeSuperdoc(), 100);
+ document.body.appendChild(script);
+
+ return () => superdocRef.current?.destroy?.();
+ }, []);
+
+ const getEditor = () => {
+ return superdocRef.current?.activeEditor || superdocRef.current?.editor;
+ };
+
+ const trackDocument = async (name) => {
+ const editor = getEditor();
+ if (!editor) {
+ setCurrentDoc({ name, identifier: null, hasGuid: false });
+ return;
+ }
+
+ let identifier = null;
+ let guid = null;
+
+ try {
+ identifier = await editor.getDocumentIdentifier();
+ guid = editor.getDocumentGuid();
+ } catch (e) {
+ addLog(`Error: ${e?.message || e}`);
+ }
+
+ if (identifier) {
+ setDocuments((prev) => {
+ const next = new Map(prev);
+ if (next.has(identifier)) {
+ const existing = next.get(identifier);
+ next.set(identifier, { ...existing, opens: existing.opens + 1 });
+ addLog(`Re-opened "${name}" — same identifier, still counts as 1`);
+ } else {
+ next.set(identifier, { name, opens: 1, hasGuid: !!guid });
+ addLog(`New document "${name}" — identifier: ${identifier.slice(0, 12)}...`);
+ }
+ return next;
+ });
+ }
+
+ setCurrentDoc({ name, identifier, hasGuid: !!guid });
+ };
+
+ const initializeSuperdoc = (file = null) => {
+ if (superdocRef.current) {
+ superdocRef.current.destroy?.();
+ }
+
+ setReady(false);
+
+ const config = {
+ selector: `#${containerIdRef.current}`,
+ telemetry: { enabled: false },
+ onReady: async () => {
+ setReady(true);
+ if (file) {
+ await trackDocument(file.name);
+ }
+ },
+ };
+
+ if (file) {
+ config.document = { data: file, type: 'docx' };
+ } else {
+ config.html = '
Upload a DOCX file to see how document counting works.