WebScript (notifications directly from the page)
WebScript is a lightweight way to receive notifications directly from someone else’s/your
web page without touching the backend. You create a “script” in Notifly,
specify the event type (page open / form submit / button click /
console errors) — and get a ready HTML snippet with a public URL like
/script/T<token>.
Insert it into your site template, a widget, an email, or a README — every time the event fires, a notification arrives in the selected channel.
Useful for:
- sites on “boxed” CMS where it’s hard to reach the backend;
- landing pages and static forms (Tilda, GitHub Pages, Netlify);
- quick experiments (“I want a ping every time someone clicks Buy”);
- widgets in third-party SaaS admin panels where you can only insert
<script>; - replacing Sentry/Bugsnag for pet projects and small teams.
Script types
Section titled “Script types”Four predefined templates are supported. You can copy the snippet with ready HTML/JS directly from the admin — here is only a description of the behavior.
scriptType | When it fires | What goes into message |
|---|---|---|
page_open | On page load (<script> is executed) | Страница: <title> + URL: <location> |
form_submit | On submit of any <form> on the page | All non-empty form fields (name: value) |
button_click | On click of an element with the data-notifly attribute | The data-notifly attribute text or the text content + URL |
console_errors | When a JS error occurs in the browser | Stacktrace, URL, User-Agent, lineno/colno |
All four variants POST to /script/T<token> with an application/json body
{title, message} — this is a public endpoint that requires no auth
(authentication is the T<token> in the URL itself).
Creating a script
Section titled “Creating a script”Via the admin
Section titled “Via the admin”- Open app.notifly.ru → Web Scripts.
- Click “Create script”, fill in:
- Name — shown label, e.g. “Landing — Buy click”.
- Channel — where to send notifications.
- Event type —
Page open,Form submitorButton click. - Notification title — default
titleof the message. - Priority — 0 = use channel
defaultPriority, otherwise 1–10.
- After creation click “Show snippet” — there you’ll find the ready HTML to
copy into the site’s
<head>or any place in<body>.
Via the REST API
Section titled “Via the REST API”curl -X POST "$NOTIFLY_URL/web-script" \ -H "Content-Type: application/json" \ -H "X-Notifly-Key: <client-token>" \ -d '{ "name": "Лендинг — клик «Купить»", "appId": 12345, "scriptType": "button_click", "title": "Клик «Купить» на лендинге", "priority": 7 }'For console_errors:
curl -X POST "$NOTIFLY_URL/web-script" \ -H "Content-Type: application/json" \ -H "X-Notifly-Key: <client-token>" \ -d '{ "name": "Production — ошибки фронтенда", "appId": 12345, "scriptType": "console_errors", "title": "JS Error", "priority": 8 }'The response will include the script object with a public token (prefix T):
{ "id": 8, "token": "T7c2a8f3b1e0d4a6c8e9f", "appId": 12345, "appName": "Marketing", "name": "Лендинг — клик «Купить»", "scriptType": "button_click", "title": "Клик «Купить» на лендинге", "priority": 7, "created": "2026-04-30T10:11:12Z", "lastUsed": null}The full trigger URL is ${NOTIFLY_URL}/script/T7c2a8f3b1e0d4a6c8e9f.
Ready snippets
Section titled “Ready snippets”All three snippets are variations of the same fetch(url, {method: "POST", ...}).
They never fail on the site side (.catch(function(){})) and do not
require CORS configuration because /script/:token always responds 200 OK.
page_open — ping on page open
Section titled “page_open — ping on page open”<!-- Notifly — notification on page open --><script>(function() { fetch("https://your-notifly/script/T7c2a8f3b1e0d4a6c8e9f", { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({ title: "Лендинг — клик «Купить»", message: "Страница: " + document.title + "\nURL: " + location.href }) }).catch(function(){});})();</script>form_submit — ping on form submit
Section titled “form_submit — ping on form submit”The script attaches to all <form> on the page, collects all non-empty
fields and sends them in the notification body:
<script>document.addEventListener("DOMContentLoaded", function() { document.querySelectorAll("form").forEach(function(form) { form.addEventListener("submit", function() { var fd = new FormData(form); var lines = []; fd.forEach(function(v, k) { if (v) lines.push(k + ": " + v); }); fetch("https://your-notifly/script/T...", { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({ title: "Заявка с сайта", message: lines.join("\n") || "(пустая форма)" }) }).catch(function(){}); }); });});</script>button_click — ping on element click
Section titled “button_click — ping on element click”Fires only for elements that have the data-notifly attribute:
<script>document.addEventListener("click", function(e) { var el = e.target.closest("[data-notifly]"); if (!el) return; var label = el.getAttribute("data-notifly") || el.textContent.trim() || "кнопка"; fetch("https://your-notifly/script/T...", { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({ title: "Клик «Купить»", message: "Клик: " + label + "\nСтраница: " + location.href }) }).catch(function(){});});</script><!-- Example usage: --><button data-notifly="Заказать">Заказать</button>console_errors — capture console errors (Sentry-lite)
Section titled “console_errors — capture console errors (Sentry-lite)”The most powerful script type — a full JS error catcher working as
a lightweight alternative to Sentry / Bugsnag / TrackJS. It uses the hosted SDK
(error-sdk.js) served from the Notifly CDN — a ready-made snippet with your token
is generated in the admin panel on the script card (“Code examples”). The SDK captures:
window.onerror— all uncaught exceptions (including resource-load errors);unhandledrejection— rejected promises without.catch();- manual calls —
ErrorSDK.captureException(err, ctx)andErrorSDK.captureMessage(msg, level, ctx).
Features:
- Breadcrumbs — the SDK buffers
console.*,fetch/XHR, clicks and SPA navigation; the last breadcrumbs ride along with the error (“what happened before it”). - Batching — errors accumulate in a queue and are sent in batches (up to 10 items or every 3s).
- Deduplication — the same error (by fingerprint) is not sent repeatedly within a ~10s window.
- Sampling —
sampleRate(1 = all) thins out client-side sending; the server extrapolates counters. - Request limit — maximum 100 POSTs per page load to avoid endless loops.
- sendBeacon — uses
navigator.sendBeaconon page leave (doesn’t block navigation). - Final flush — on
pagehide/ tab hide the script sends the remaining queue. - Sanitization — secrets (password/token/cookie/…) and sensitive query params are scrubbed on the client AND again on the server.
- 1 batch = 1 quota event — the whole array of errors becomes a single notification with one push.
<!-- Notifly — capture browser errors (hosted SDK) --><script src="https://app.notifly.ru/static/error-sdk.js" crossorigin="anonymous"></script><script> // window.NOTIFLY_RELEASE can be set BEFORE this script to tag events with a release. window.ErrorSDK && window.ErrorSDK.init({ endpoint: "https://your-notifly/script/T...", app: "My App", release: (window.NOTIFLY_RELEASE || ""), environment: "production", sampleRate: 1, maxBreadcrumbs: 20 });</script>init() parameters:
| Parameter | Description |
|---|---|
endpoint | Public script URL POST /script/T<token> (required) |
app | Application name — goes into the notification title |
release | Release tag (git-sha / version) — required for source maps |
environment | Environment (production / staging / …) — checked against the script’s allowlist |
sampleRate | Fraction of errors to send, 1 = all |
notifyThreshold | Client-side hint for the notify threshold (the server decides anyway) |
maxBreadcrumbs | How many breadcrumbs to attach per error (≤ 30) |
Data format
Section titled “Data format”The SDK builds the batch for you; the schema below is for custom integrations.
Request body POST /script/T<token> (max 64 KiB, up to 50 errors per batch):
{ "title": "My App", "release": "v1.2.3", "environment": "production", "sampleRate": 1, "flushReason": "batch", "errors": [ { "eventId": "3f2a…", "type": "error", "level": "error", "message": "Cannot read properties of undefined (reading 'map')", "stack": "TypeError: Cannot read properties...\n at App.tsx:42:12\n at ...", "url": "https://example.com/dashboard", "lineno": 42, "colno": 12, "userAgent": "Mozilla/5.0 ...", "ts": 1716556800000, "sentAt": "2026-07-14T12:00:00.000Z", "exception": {"name": "TypeError", "message": "...", "stack": "..."}, "runtime": { "url": "https://example.com/dashboard", "path": "/dashboard", "referrer": "https://example.com/", "userAgent": "Mozilla/5.0 ...", "language": "en-US", "viewport": "1920x1080" }, "context": {"feature": "checkout"}, "breadcrumbs": [ {"category": "ui.click", "level": "info", "message": "", "data": {"target": "button#pay"}, "timestamp": "2026-07-14T11:59:58.000Z"} ] } ]}Required fields per error: message, type (error | unhandledrejection |
console.error | exception | message), level (info | warning | error |
fatal), runtime.url and sentAt (RFC3339). A batch without environment, with an
unknown type/level or with more than 30 breadcrumbs per error is rejected with 400.
Notifly server forms one Sentry-style notification card from the batch:
- Title — the first error (
TypeError: …, +(+N more)if several errors). - Body — tags (level · environment · release · browser · language), URL:line:col, referrer/viewport, the first error’s stacktrace, a compact list of the rest (up to 4), the last breadcrumbs and the issue occurrence counter.
Release binding
Section titled “Release binding”Set window.NOTIFLY_RELEASE before loading the script — the value will
be included in the release field of each batch:
<script>window.NOTIFLY_RELEASE = "v2.1.0-abc1234";</script><!-- Notifly console_errors snippet here -->Where to insert in popular frameworks
Section titled “Where to insert in popular frameworks”| Framework | Where to place |
|---|---|
| Next.js (App Router) | app/layout.tsx in <head> via next/script strategy="beforeInteractive" |
| Next.js (Pages) | pages/_document.tsx in <Head> |
| React (CRA / Vite) | public/index.html in <head> before the bundle |
| Vue / Nuxt | nuxt.config → app.head.script or app.html |
| Angular | src/index.html in <head> before polyfills |
| Astro | src/layouts/Layout.astro in <head> |
| Electron | In the renderer HTML <head> |
Source maps — unfolding minified stack
Section titled “Source maps — unfolding minified stack”If your JS was minified (esbuild, terser, Webpack production), stacktraces
in the browser look like at f (https://app.example.com/static/js/main.abc123.js:1:12345) —
without function names and real lines. Notifly can on-the-fly map such frames
back to sources (TypeScript / .vue / .jsx) using uploaded source maps.
How it works
Section titled “How it works”- Build your frontend with
.mapfiles enabled (sourceMap: true). - Upload these
.mapfiles to Notifly via the REST API — one map per.jsfile. - Provide the same
releasefor each.mapas you set inwindow.NOTIFLY_RELEASE. - On error receipt Notifly finds the
.mapbyrelease+ URL prefix + file name and replacesURL:LINE:COLwithsrc/file.ts:LINE:COL— directly in the notification text.
Resolving is best-effort: if the map isn’t found or the frame doesn’t match, the notification contains the original (minified) stack.
Upload via REST
Section titled “Upload via REST”# basic auth — same login/password as for the admincurl -u admin:admin \ -F "release=v2.1.0-abc1234" \ -F "urlPrefix=https://app.example.com/static/js/" \ -F "fileName=main.abc123.js" \ -F "file=@dist/static/js/main.abc123.js.map" \ "$NOTIFLY_URL/web-script/<scriptId>/sourcemaps"The request body is multipart/form-data with required fields:
| Field | Description |
|---|---|
file | The .map file itself (JSON Source Map v3, up to 10 MB) |
release | Release version — must match window.NOTIFLY_RELEASE |
urlPrefix | URL prefix from which the browser loads this .js (e.g. https://x/static/) |
fileName | The .js file name without prefix (main.abc123.js), no / or \ |
When a frame matches by urlPrefix + fileName Notifly resolves the position. Frame lookup uses the longest matching prefix, so you can keep maps
for different domains/CDNs simultaneously.
CLI helper
Section titled “CLI helper”To avoid calling curl in a loop, use the ready script
scripts/upload-sourcemaps.py:
python3 scripts/upload-sourcemaps.py \ --api https://api.notifly.ru \ --user "$NOTIFLY_USER" --password "$NOTIFLY_PASS" \ --script-id 12345 \ --release "$GITHUB_SHA" \ --url-prefix "https://app.example.com/static/js/" \ --dir ./dist/static/jsExample GitHub Actions — upload maps after build:
- name: Build run: npm run build
- name: Upload sourcemaps to Notifly env: NOTIFLY_USER: ${{ secrets.NOTIFLY_USER }} NOTIFLY_PASSWORD: ${{ secrets.NOTIFLY_PASSWORD }} run: | python3 scripts/upload-sourcemaps.py \ --script-id ${{ vars.NOTIFLY_SCRIPT_ID }} \ --release "${{ github.sha }}" \ --url-prefix "https://app.example.com/static/js/" \ --dir ./dist/static/jsManagement and REST
Section titled “Management and REST”| Method and path | Purpose |
|---|---|
GET /web-script/:id/sourcemaps | list uploaded maps |
POST /web-script/:id/sourcemaps | upload a .map (multipart) |
DELETE /web-script/:id/sourcemaps/:smid | delete a map (S3 + DB) |
In the admin the “Source maps” button appears on a console_errors script card — there you can view the list, delete outdated maps
or upload a new .map without the CLI.
Security and limits
Section titled “Security and limits”- Maps are stored in a private S3 bucket (Yandex Object Storage), they have no direct public URL.
- Content is deduplicated by
sha256— re-uploading the same file won’t create duplicates. - Maximum size of a single
.mapis 10 MB. - Deleting a web script removes all related
.mapfiles automatically (cascade). - Resolving is limited by a 3-second timeout per request — if there are many maps, extras are ignored and the original stack still appears in the notification.
Triggering manually
Section titled “Triggering manually”You can hit /script/T<token> with anything — this endpoint is public
and accepts GET and POST. A JSON body is optional: if absent, the push
will use the script’s title and the text “(web script trigger)”.
# minimalcurl -fsS "$NOTIFLY_URL/script/T7c2a8f3b1e0d4a6c8e9f" -o /dev/null
# override title and messagecurl -X POST "$NOTIFLY_URL/script/T7c2a8f3b1e0d4a6c8e9f" \ -H "Content-Type: application/json" \ -d '{"title":"Заявка №42","message":"Имя: Иван\nТелефон: +7..."}'REST API
Section titled “REST API”| Method and path | Authorization | Purpose |
|---|---|---|
GET /web-script | client-token | list scripts |
POST /web-script | client-token (write) | create |
PUT /web-script/:id | client-token (write) | update |
DELETE /web-script/:id | client-token (write) | delete |
GET /web-script/:id/sourcemaps | client-token | list source maps |
POST /web-script/:id/sourcemaps | client-token (write) | upload .map |
DELETE /web-script/:id/sourcemaps/:smid | client-token (write) | delete .map |
GET /web-script/:id/issues | client-token | list aggregated errors |
GET /web-script/:id/issue-count | client-token | number of unresolved issues ({"unresolved": N}) |
POST /web-script/:id/issues/:iid/resolve | client-token (write) | mark resolved |
POST /web-script/:id/issues/:iid/ignore | client-token (write) | ignore the issue |
DELETE /web-script/:id/issues/:iid | client-token (write) | delete |
GET /web-script/:id/issues/:iid/breadcrumbs | client-token | user actions before the error |
POST /web-script/:id/issues/:iid/analyze | client-token (write) | AI root-cause analysis (body: lang, force) |
GET /web-script/:id/issues/:iid/analysis | client-token | the stored analysis (204 if there is none) |
GET /web-script/:id/series | client-token | time series of script triggers |
GET /web-script/:id/error-series | client-token | time series of console_errors |
GET /web-script/:id/flush-stats | client-token | distribution of batch flush reasons |
GET/POST /script/:token | public | trigger from browser or script |
The “Issues” section on a console_errors script card groups incoming
errors into deduplicated problems (by message + top stack frame +
release): for each you see the count, first and last occurrence. A problem can be marked resolved (/issues/:iid/resolve) — its next occurrence will generate a notification again as a regression — hidden (/issues/:iid/ignore) or deleted
(DELETE /web-script/:id/issues/:iid).
AI analysis of a problem
Section titled “AI analysis of a problem”POST /web-script/:id/issues/:iid/analyze gathers the evidence (stack,
breadcrumbs, page context), sends it to the model and stores the analysis — the
likely cause and what to check first:
curl -X POST "$NOTIFLY_URL/web-script/77/issues/1234/analyze" \ -H "Content-Type: application/json" \ -H "X-Notifly-Key: <client-token>" \ -d '{"lang": "en"}'The analysis is cached by a fingerprint of its inputs: while the problem has no
new events and the language has not changed, a repeated call returns the stored
result — without calling the model and without charging the
AI quota. To force a recomputation pass
{"force": true}. The stored analysis is available separately via
GET /web-script/:id/issues/:iid/analysis (204 if there is none yet).
Activity charts
Section titled “Activity charts”Time series accept the common parameters ?bucket=1m|1h|1d&from=&to=; the window
is clamped to the retention depth.
/series— triggers of trigger-type scripts (page_open,form_submit,button_click); forconsole_errorsit returns empty./error-series— the dynamics ofconsole_errors./flush-stats— the distribution of batch flush reasons (reason → count): shows what share of events leaves viabeaconwhen the page is closed.
Security
Section titled “Security”T<token>is generated with a crypto-secure random generator (160 bits) — including it in the URL is as safe as you are at not committing the token to a public repository.- If you accidentally committed
T<token>to a public repo — delete the script via the admin orDELETE /web-script/:id. The old URL will start “absorbing” requests (silent200 OK) without creating notifications. - On each successful trigger the
lastUsedfield is updated — the admin shows if a script is “hanging” unused.