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)Page: <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 browserAn error card: stacktrace, URL, browser, breadcrumbs

The trigger snippets (page_open, form_submit, button_click) POST to /script/T<token> with an application/json body {title, message}, console_errors sends a batch of errors (format below). This is a public endpoint that requires no auth (authentication is the T<token> in the URL itself).

Every notification created is one event of the daily quota. It is an ordinary channel message: escalation, quiet hours and the channel’s maintenance windows apply to it.

  1. Open app.notifly.ru → Integrations → Website scripts.
  2. Click “Create script”, fill in:
    • Name — shown label, e.g. “Landing — Buy click”.
    • Channel — where to send notifications (only your own channel).
    • Script type — “Page open”, “Form submit”, “Button click” or “Console errors”. The type cannot be changed after creation.
    • Notification title — default title of the message.
    • Priority (0-10) — 0 = use the channel’s defaultPriority.
    • For “Console errors” — also Notify threshold and Environments (see below).
  3. The script card shows ready code in tabs: HTML embed, SDK in code (console errors only), Google Tag Manager and For LLM. The HTML embed can be copied into the site’s <head> or any place in <body>.
Окно терминала
curl -X POST "$NOTIFLY_URL/web-script" \
-H "Content-Type: application/json" \
-H "X-Notifly-Key: <api-token>" \
-d '{
"name": "Landing — Buy click",
"appId": 12345,
"scriptType": "button_click",
"title": "Buy click on the landing",
"priority": 3
}'

For console_errors:

Окно терминала
curl -X POST "$NOTIFLY_URL/web-script" \
-H "Content-Type: application/json" \
-H "X-Notifly-Key: <api-token>" \
-d '{
"name": "Production — frontend errors",
"appId": 12345,
"scriptType": "console_errors",
"title": "JS Error",
"priority": 8,
"notifyThreshold": "error",
"environments": "production"
}'

The response will include the script object with a public token (prefix T):

{
"id": 8,
"token": "T7c2a8f3b1e0d4a6c8e9f",
"appId": 12345,
"appName": "Marketing",
"name": "Landing — Buy click",
"scriptType": "button_click",
"title": "Buy click on the landing",
"priority": 3,
"notifyThreshold": "",
"environments": "",
"created": "2026-04-30T10:11:12Z",
"lastUsed": null
}

The full trigger URL is ${NOTIFLY_URL}/script/T7c2a8f3b1e0d4a6c8e9f (the examples use a shortened token; a real one is T plus 40 hex characters).

Required fields are name (up to 200 characters), appId and scriptType; priority is 0–10. PUT /web-script/:id takes the same body, and its scriptType must match the current one. An account can have up to 500 scripts.

All three trigger snippets are variations of the same fetch(url, {method: "POST", ...}). They never break the page (.catch(function(){})) and do not require CORS configuration: /script/:token answers with permissive CORS headers for any domain.

<!-- Notifly — notification on page open -->
<script>
(function () {
fetch('https://api.notifly.ru/script/T7c2a8f3b1e0d4a6c8e9f', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
title: 'Landing — Buy click',
message: 'Page: ' + document.title + '\n\nURL: ' + location.href,
}),
}).catch(function () {});
})();
</script>

The script attaches to all <form> elements 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://api.notifly.ru/script/T...', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
title: 'Website request',
message: lines.join('\n\n') || '(empty form)',
}),
}).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() || 'button';
fetch('https://api.notifly.ru/script/T...', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
title: 'Buy click',
message: 'Click: ' + label + '\n\nPage: ' + location.href,
}),
}).catch(function () {});
});
</script>
<!-- Usage example: -->
<button data-notifly="Order">Order</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. The SDK captures:

  • window.onerror — all uncaught exceptions (including resource-load errors);
  • unhandledrejection — rejected promises without .catch();
  • console.error(...) — turned into a full event, even without a throw;
  • CSP violations (securitypolicyviolation) — as a warning-level event;
  • manual calls — ErrorSDK.captureException(err, ctx) and ErrorSDK.captureMessage(msg, level, ctx).

Features:

  • Breadcrumbs — the SDK buffers console.error/warn/log/info calls, fetch/XHR, clicks and SPA navigation; the latest crumbs are attached to the error (“what happened before it”).
  • Batching — errors are queued and sent in batches (up to 10 items or every 3 s).
  • Client-side deduplication — the same error is not re-sent within a ~10 s window.
  • Server-side deduplication — a notification for the same issue arrives at most once every 2 minutes; repeats within that window only increase the counter.
  • Sampling — sampleRate (1 = everything) thins sending on the client; the server extrapolates counters.
  • Request limit — at most 100 POSTs per page load, to avoid infinite loops.
  • sendBeacon — navigator.sendBeacon is used when leaving the page (does not block navigation).
  • Final flush — on pagehide / tab hidden the script sends the remaining queue.
  • Sanitization — secrets (password/token/cookie/…) and sensitive query parameters are stripped both on the client and again on the server.
  • 1 batch = at most one notification and one quota event. If there is nothing to notify about (everything is a repeat within the deduplication window, below the threshold or from a foreign environment), the server answers {"ok": true, "deduped": true} and no quota is spent.
<!-- Notifly — browser error capture (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://api.notifly.ru/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 batch’s title field (the notification title is built from the first error)
releaseRelease tag (git sha / version) — needed for source maps
environmentEnvironment (production / staging / …, production by default) — checked against the script’s allowlist
sampleRateShare of errors to send, 1 = all
notifyThresholdClient-side hint for the notification threshold (the server decides by the script setting)
maxBreadcrumbsHow many “crumbs” to attach to an error (20 by default; the server rejects more than 30)
ignoreErrorsDo not send errors whose message matches (string — substring, RegExp — test)
denyUrls / allowUrlsFilter by the URL the error comes from (the top stack frame)
beforeSendfunction (event) { … } — the last hook before sending; null = do not send

Events filtered on the client never reach the server — they spend no quota and do not show up in issues or charts.

The SDK builds the batch itself; below is the schema for custom integrations. Request body of 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 of each 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, with more than 50 errors or more than 30 breadcrumbs per error is rejected with 400; a body over 64 KiB gets 413. The server re-sanitizes everything it receives: sensitive keys and query parameters, JWT/Bearer tokens, emails, phone numbers, card numbers and public IPs are replaced with [Filtered].

Notifly server forms one Sentry-style notification card from the batch:

  • Title — the first error (TypeError: …, + (+N more) if several errors).
  • Body (Markdown) — tags (level · environment · release · browser · language), URL:line:col, referrer/viewport, the first error’s stacktrace (already resolved via source maps), a compact list of the rest (up to 4), the last breadcrumbs (up to 3), the issue’s occurrence count and when it was first seen.

A console_errors script has two server-side settings (in the admin UI — the “Notify threshold” and “Environments” fields, in the API — notifyThreshold and environments):

  • notifyThreshold — info, warning, error or fatal: notify only about errors of this level and above. Empty — any level. Errors below the threshold are still grouped into issues and shown on the chart, just without a push.
  • environments — a comma-separated allowlist of environments, e.g. production,staging. Empty — any environment. A batch from an environment not on the list is dropped entirely: it reaches neither issues nor charts.

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.config → app.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. Give each .map the same release you pass in window.NOTIFLY_RELEASE.
  4. When an error arrives, Notifly finds the .map by release + URL prefix + file name and replaces URL:LINE:COL with src/file.ts:LINE:COL — right in the notification text.

Resolving is best-effort: if the map is not found or the frame does not match, the notification gets the original (minified) stack.

Окно терминала
# API token with write access
curl -H "X-Notifly-Key: <api-token>" \
-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"

Instead of a token this endpoint — the only one in the API — also accepts the account’s login and password (HTTP Basic, curl -u "$NOTIFLY_USER:$NOTIFLY_PASSWORD"): handy for older CI scripts. Source maps are accepted only for console_errors scripts.

The request body is multipart/form-data with the required fields:

FieldDescription
fileThe .map file itself (JSON Source Map v3, up to 10 MB)
releaseRelease version — must match window.NOTIFLY_RELEASE
urlPrefixURL prefix the browser loads this .js from (e.g. https://x/static/)
fileNameName of the .js file without the prefix (main.abc123.js), no / and \

When a frame matches urlPrefix + fileName, Notifly resolves the position. Frame lookup uses the longest prefix, so you can keep maps for several domains / CDNs at the same time. The file is validated on upload: not a Source Map v3 — 400, over 10 MB — 413; the anti-XSSI prefix )]}' is stripped automatically.

If your build already talks to Notifly from code, the upload is one SDK call with an API token (write access), no basic auth:

f, _ := os.Open("dist/static/js/main.abc123.js.map")
defer f.Close()
_, err := admin.UploadSourceMap(ctx, scriptID, notifly.SourceMapUpload{
Release: os.Getenv("GITHUB_SHA"), // must match window.NOTIFLY_RELEASE
URLPrefix: "https://app.example.com/static/js/",
FileName: "main.abc123.js",
Map: f,
})
with open("dist/static/js/main.abc123.js.map", "rb") as f:
admin.upload_source_map(script_id, os.environ["GITHUB_SHA"],
"https://app.example.com/static/js/", "main.abc123.js", f)

Re-uploading the same (release, fileName) pair replaces the map rather than creating duplicates, so re-running the build is safe. The SDK checks the file size and name before sending.

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 \
--token "$NOTIFLY_TOKEN" \
--script-id 12345 \
--release "$GITHUB_SHA" \
--url-prefix "https://app.example.com/static/js/" \
--dir ./dist/static/js

The script takes an API token from --token / NOTIFLY_TOKEN, or a login and password from --user/--password (NOTIFLY_USER/NOTIFLY_PASSWORD). A GitHub Actions example — uploading maps after the build:

- name: Build
run: npm run build
- name: Upload sourcemaps to Notifly
env:
NOTIFLY_TOKEN: ${{ secrets.NOTIFLY_TOKEN }}
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, at most 500 maps per script.
  • 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: without it the push gets the title from the script settings and the text “(web script trigger)”.

Окно терминала
# 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":"Request #42","message":"Name: John\nPhone: +1..."}'

Responses: 200 {"ok": true} — the notification is created; 400 — unknown token or invalid body; 413 — body over 64 KiB; 429 — the daily quota is exhausted or too many requests; 503 — a temporary failure, the request can be retried.

Method & pathAuthorizationPurpose
GET /web-scriptAPI tokenlist scripts
POST /web-scriptAPI token (write)create
PUT /web-script/:idAPI token (write)update
DELETE /web-script/:idAPI token (write)delete
GET /web-script/:id/sourcemapsAPI tokenlist source maps
POST /web-script/:id/sourcemapsAPI token (write) or Basicupload a .map
DELETE /web-script/:id/sourcemaps/:smidAPI token (write)delete a .map
GET /web-script/:id/issuesAPI tokenlist of aggregated errors
GET /web-script/:id/issue-countAPI tokennumber of unresolved issues ({"unresolved": N})
POST /web-script/:id/issues/:iid/resolveAPI token (write)mark as resolved
POST /web-script/:id/issues/:iid/ignoreAPI token (write)mute the issue
DELETE /web-script/:id/issues/:iidAPI token (write)delete
GET /web-script/:id/issues/:iid/breadcrumbsAPI tokenthe user’s last actions before the error
POST /web-script/:id/issues/:iid/analyzeAPI tokenAI root-cause analysis (body: lang, force)
GET /web-script/:id/issues/:iid/analysisAPI tokenthe saved analysis (204 if there is none yet)
GET /web-script/:id/seriesAPI tokentime series of script triggers
GET /web-script/:id/error-seriesAPI tokentime series of console_errors errors
GET /web-script/:id/flush-statsAPI tokendistribution of batch send reasons
GET/POST /script/:tokenpublictrigger from a browser or a script

Channel tokens (including shared ones) are not accepted by the management endpoints — 403.

The “Issues” section on a console_errors script card groups incoming errors into deduplicated issues. The grouping key is the error type, up to five top stack frames (after source maps; line numbers without columns, URLs without the query and with ids in the path generalized) and the release; when there is no recognizable stack — the normalized message (numbers, UUIDs, URLs replaced with placeholders) and the page URL. For each issue you see the occurrence count (and an estimate adjusted for sampleRate — estimatedCount), first and last seen times, a sample stack, URL and browser; “Events before the error” (breadcrumbs) are kept for ~14 days, and the issues themselves are deleted after 90 days without new events.

  • Mark as resolved (/issues/:iid/resolve) — the next occurrence notifies again as a regression. The body {"resolved": false} reopens the issue.
  • Mute notifications (/issues/:iid/ignore) — events are still counted, but no notifications about the issue ever arrive. {"ignored": false} turns them back on.
  • Delete — DELETE /web-script/:id/issues/:iid.

issue-count excludes resolved and muted issues — it is the number for the badge on the card.

POST /web-script/:id/issues/:iid/analyze collects evidence (stack, breadcrumbs, page context), hands it to the model and saves the analysis — the probable 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: <api-token>" \
-d '{"lang": "en"}'

Response:

{
"probableCause": "…",
"evidence": ["…"],
"suggestedSteps": ["…"],
"confidence": "medium",
"model": "…",
"lang": "en",
"created": "2026-09-27T10:00:00Z",
"stale": false
}

lang is the analysis language (ru by default), confidence is high, medium or low; evidence and suggestedSteps hold up to five items.

The analysis is cached by a fingerprint of its inputs: while the issue has no new events and the language is unchanged, a repeated call returns the saved result — without calling the model and without charging the AI-helper quota (aiRequestsPerDay, see quotas). To recompute, pass {"force": true}. The ready analysis is also available via GET /web-script/:id/issues/:iid/analysis (204 if there is none yet; stale: true — the issue got new events after the analysis).

Errors: 422 — the issue has neither a stack nor breadcrumbs, nothing to analyse; 429 — the daily AI request quota is exhausted; 503 — AI is not configured; 502 — the model did not answer or returned an invalid result.

Time series take the common parameters ?bucket=1m|1h|1d&from=&to= (RFC3339); by default — 1h over the last 24 hours, the window is at most 35 days. Minute data is kept for about a day and daily data for about a month, so use 1d for long windows.

  • /series — triggers of trigger scripts (page_open, form_submit, button_click); for console_errors it returns empty.
  • /error-series — the dynamics of console_errors errors broken down by level (info, warning, error, fatal); every accepted event counts, including those below the notification threshold.
  • /flush-stats — the distribution of batch send reasons (reason → count: batch, timer, pagehide, visibilitychange, manual, …): shows what share of batches is sent while the page is closing (via sendBeacon).
  • T<token> is generated by a cryptographically secure random generator (160 bits). It is inevitably visible in the page code, so treat it as a “public ingest key”: all it allows is sending events to this script.
  • The token of an existing script cannot be changed. If the token is abused, delete the script in the admin or via DELETE /web-script/:id and create a new one: requests to the old URL will get 400 and create no notifications.
  • The lastUsed field is updated on every successful trigger — the admin shows when a script sits unused.