Skip to content

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.

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.

scriptTypeWhen it firesWhat goes into message
page_openOn page load (<script> is executed)Страница: <title> + URL: <location>
form_submitOn submit of any <form> on the pageAll non-empty form fields (name: value)
button_clickOn click of an element with the data-notifly attributeThe data-notifly attribute text or the text content + URL
console_errorsWhen a JS error occurs in the browserStacktrace, 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).

  1. Open app.notifly.ruWeb Scripts.
  2. Click “Create script”, fill in:
    • Name — shown label, e.g. “Landing — Buy click”.
    • Channel — where to send notifications.
    • Event typePage open, Form submit or Button click.
    • Notification title — default title of the message.
    • Priority — 0 = use channel defaultPriority, otherwise 1–10.
  3. After creation click “Show snippet” — there you’ll find the ready HTML to copy into the site’s <head> or any place in <body>.
Terminal window
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:

Terminal window
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.

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.

<!-- 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>

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>

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 callsErrorSDK.captureException(err, ctx) and ErrorSDK.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.
  • SamplingsampleRate (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.sendBeacon on 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:

ParameterDescription
endpointPublic script URL POST /script/T<token> (required)
appApplication name — goes into the notification title
releaseRelease tag (git-sha / version) — required for source maps
environmentEnvironment (production / staging / …) — checked against the script’s allowlist
sampleRateFraction of errors to send, 1 = all
notifyThresholdClient-side hint for the notify threshold (the server decides anyway)
maxBreadcrumbsHow many breadcrumbs to attach per error (≤ 30)

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.

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 -->
FrameworkWhere 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 / Nuxtnuxt.configapp.head.script or app.html
Angularsrc/index.html in <head> before polyfills
Astrosrc/layouts/Layout.astro in <head>
ElectronIn the renderer HTML <head>

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.

  1. Build your frontend with .map files enabled (sourceMap: true).
  2. Upload these .map files to Notifly via the REST API — one map per .js file.
  3. Provide the same release for each .map as you set in window.NOTIFLY_RELEASE.
  4. On error receipt Notifly finds the .map by release + URL prefix + file name and replaces URL:LINE:COL with src/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.

Terminal window
# basic auth — same login/password as for the admin
curl -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:

FieldDescription
fileThe .map file itself (JSON Source Map v3, up to 10 MB)
releaseRelease version — must match window.NOTIFLY_RELEASE
urlPrefixURL prefix from which the browser loads this .js (e.g. https://x/static/)
fileNameThe .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.

To avoid calling curl in a loop, use the ready script scripts/upload-sourcemaps.py:

Terminal window
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/js

Example 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/js
Method and pathPurpose
GET /web-script/:id/sourcemapslist uploaded maps
POST /web-script/:id/sourcemapsupload a .map (multipart)
DELETE /web-script/:id/sourcemaps/:smiddelete 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.

  • 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 .map is 10 MB.
  • Deleting a web script removes all related .map files 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.

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)”.

Terminal window
# minimal
curl -fsS "$NOTIFLY_URL/script/T7c2a8f3b1e0d4a6c8e9f" -o /dev/null
# override title and message
curl -X POST "$NOTIFLY_URL/script/T7c2a8f3b1e0d4a6c8e9f" \
-H "Content-Type: application/json" \
-d '{"title":"Заявка №42","message":"Имя: Иван\nТелефон: +7..."}'
Method and pathAuthorizationPurpose
GET /web-scriptclient-tokenlist scripts
POST /web-scriptclient-token (write)create
PUT /web-script/:idclient-token (write)update
DELETE /web-script/:idclient-token (write)delete
GET /web-script/:id/sourcemapsclient-tokenlist source maps
POST /web-script/:id/sourcemapsclient-token (write)upload .map
DELETE /web-script/:id/sourcemaps/:smidclient-token (write)delete .map
GET /web-script/:id/issuesclient-tokenlist aggregated errors
GET /web-script/:id/issue-countclient-tokennumber of unresolved issues ({"unresolved": N})
POST /web-script/:id/issues/:iid/resolveclient-token (write)mark resolved
POST /web-script/:id/issues/:iid/ignoreclient-token (write)ignore the issue
DELETE /web-script/:id/issues/:iidclient-token (write)delete
GET /web-script/:id/issues/:iid/breadcrumbsclient-tokenuser actions before the error
POST /web-script/:id/issues/:iid/analyzeclient-token (write)AI root-cause analysis (body: lang, force)
GET /web-script/:id/issues/:iid/analysisclient-tokenthe stored analysis (204 if there is none)
GET /web-script/:id/seriesclient-tokentime series of script triggers
GET /web-script/:id/error-seriesclient-tokentime series of console_errors
GET /web-script/:id/flush-statsclient-tokendistribution of batch flush reasons
GET/POST /script/:tokenpublictrigger 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).

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:

Terminal window
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).

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); for console_errors it returns empty.
  • /error-series — the dynamics of console_errors.
  • /flush-stats — the distribution of batch flush reasons (reason → count): shows what share of events leaves via beacon when the page is closed.
  • 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 or DELETE /web-script/:id. The old URL will start “absorbing” requests (silent 200 OK) without creating notifications.
  • On each successful trigger the lastUsed field is updated — the admin shows if a script is “hanging” unused.