Developer docs
Widget reference
The widget is one script tag on your staging site. It stays invisible to ordinary visitors, wakes only for an invited tester, and mounts inside a closed Shadow DOM so its styles cannot leak into your page and your page's scripts cannot reach into it. Every request it makes is outbound from the tester's browser to DeploySeal — DeploySeal never connects to your staging network, so private, VPN-gated and password-protected environments work unchanged.
Install
Running nopCommerce or WordPress? Install the nopCommerce plugin or the WordPress plugin instead — it fills in the build marker and records your plugin inventory. For a page you cannot add a script to, such as an admin area, testers can use the bookmarklet.
Paste the snippet from Sites → your site → Environments into the <head> of every page, one tag per page. async is required; it never blocks rendering. Once the first heartbeat arrives the site shows the widget as Live and testers can be invited.
<script src="https://cdn.deployseal.com/ds-widget.js" data-ds-site-key="ls_••••••••••••••••••••••••••••••••••••" data-ds-environment="staging" data-ds-build="4.90.8+a1b2c3d" async ></script>
One host. https://cdn.deployseal.com/ds-widget.js is the only supported URL: a 1 KB classic-script loader that imports the ES module beside it, split into chunks that load on demand. Do not inline it, vendor it, or pin a chunk filename — the entry is served with a five-minute cache and chunks are content-hashed, so a new widget reaches your pages within minutes of a release. The running version is on window.__deployseal.version.
Two tags on one page. If a page ends up with two tags — a plugin's and a pasted snippet, say — the tag whose loader executes configures the widget: the loader stashes its own attributes before importing the module, and the module ignores a second mount. The two need not be first-in-the-document to win, so they must agree on key, environment and build, or the evidence will be attributed to whichever happened to run. Remove one; the site page warns when two install paths are seen on one environment within a day.
Attributes
| Attribute | What it does |
|---|---|
| data-ds-site-keyrequired | Identifies the environment the page belongs to — every environment has its own public key (ls_ + 37 characters; older ds_ keys stay valid), and this key alone decides which environment the evidence lands in. Copy it from Sites → your site → Environments. It is not a secret: it only works from the environment's allowed origins, so a copied snippet is useless anywhere else. Without this attribute the widget logs a warning and does nothing. |
| data-ds-environmentoptional | Recommended. A declared label for the environment you believe this page is — not an identifier, the key is. Up to 32 characters of a–z 0–9 - after lower-casing; the widget reduces anything else to that alphabet before sending. The dashboard's snippet fills in the environment's name. It is sent with every heartbeat and recorded as the environment's last seen label; when it does not match the environment the key belongs to, the site page shows an amber warning — the check that catches a key pasted into the wrong environment. There is no default: leave it out and nothing is declared. |
| data-ds-buildoptional | Recommended. The build marker: which code is running on this page. One token, no whitespace, up to 64 characters — see Build marker below. The dashboard's snippet ships REPLACE-WITH-BUILD-MARKER, which you must replace; that placeholder is never recorded as a build. A <meta name="ds-build"> tag is read live as an alternative (the script attribute wins when both are present). |
| data-ds-api-baseoptional | Overrides the API origin. Defaults to https://api.deployseal.com. Only for self-hosted or preview deployments; the dashboard's snippet includes the right value for your deployment. |
| data-ds-installeroptional | Informational. Which install path put the tag on the page — up to 48 characters of a–z 0–9 . - /, e.g. nopcommerce-plugin/1.3.0. The pasted snippet omits it and the widget reports snippet in its place. It is sent with every heartbeat; the site page uses it to say Installed via the nopCommerce plugin, to open the install drawer on the plugin, and to warn when two install paths were both live on one environment within 24 hours. Only platform plugins and other packaged installers set it. |
Optional markup on your own elements
<!-- Optional: a stable, deploy-proof anchor for element pins --> <button data-ds-id="checkout-pay-now">Pay now</button> <!-- Optional: mask the text of an element in session replays --> <p data-ds-mask>Order for Jane Citizen, 14 Example St</p> <p class="ds-mask">…also works as a class</p>
Build marker
The build marker answers which code was under test. The readiness report compares the campaign's declared release identifier with the markers observed while testers were on the site: confirmed when one observed marker equals the identifier (case-insensitively) or one is a prefix of the other and the shorter is at least 7 characters — so a short SHA confirms a full one; mismatch when markers were seen and none match; unverified when an identifier was declared but no marker was ever seen. Two or more distinct markers in one campaign raise an amber testing spans N deployments flag.
- Prefer the commit. If a git SHA is available, the marker is the SHA (7 or more characters). The campaign can then declare the short or the full SHA.
- Otherwise a version, optionally plus a SHA: 4.90.8 or 4.90.8+a1b2c3d. Prefix rules run from the start of the string, so a1b2c3d will not confirm 4.90.8+a1b2c3d — declare in the review exactly what the page emits.
- One token, no whitespace, at most 64 characters, stable for the life of a deployment and different on every deployment. The time the page rendered is not a build marker.
- REPLACE-WITH-BUILD-MARKER — the placeholder in the dashboard's snippet — is ignored in any casing. Until you replace it, the environment shows no build and the report says Not verified by build marker rather than guessing.
Build marker via meta tag
<!-- Alternative to data-ds-build: read live on every page, so a
single-page app can update it after each deploy -->
<meta name="ds-build" content="4.90.8+a1b2c3d">Allowed origins
Every widget request carries the browser's Origin header, and the API accepts it only if it exactly matches one of the environment's allowed origins: scheme://host[:port], compared case-insensitively after trimming one trailing slash. No wildcards, no subdomain matching, no port or scheme normalisation — https://staging.example.com, http://staging.example.com and https://staging.example.com:8443 are three different origins. Requests from any other origin are rejected, which is what makes the public key safe to ship in HTML.
- An environment is its allowed-origins list — it has no hostname field. Each environment with its own key carries its own list; when that list is empty the site's list applies.
- Register one key per environment per store, never one key for everything: a campaign binds one environment, and the report infers one only when it has to.
- In production an environment with no allowed origins rejects every widget call — the dashboard warns you until you add one.
- Regenerating an environment key invalidates the old snippet immediately.
Heartbeat
On every page load the widget posts one heartbeat, fire-and-forget (60 per minute per IP). The first one that arrives from an allowed origin verifies the environment permanently; each one after that updates Last seen on the environment card: when, which build, which widget version, and the label the page declared.
POST https://api.deployseal.com/api/widget/heartbeat
{
"siteKey": "ls_…",
"pageUrl": "https://staging.example.com/checkout",
"pageTitle": "Checkout",
"environment": "staging",
"build": "4.90.8+a1b2c3d",
"widgetVersion": "0.1.0",
"installer": "snippet"
}environment must be 1–32 characters of a–z 0–9 - after lower-casing, build a single token of at most 64 characters, widgetVersion at most 32 and installer at most 48 of a–z 0–9 . - /; anything else is rejected with 400 and nothing is recorded. All four are optional — an older install that sends only the site key still verifies. The environment keeps the current installer and, while both were seen within 24 hours, the one before it.
How a tester session starts
- A tester is invited to a campaign and receives a personalised link. Accepting it sends them to your staging URL with ?ds_cti=… appended — the campaign-tester id that is their credential. No DeploySeal account is involved.
- The widget reads the parameter, stores it in sessionStorage (so it survives navigation within the tab), strips it from the address bar, and calls the bootstrap endpoint with the site key and that id.
- Bootstrap succeeds only when the origin is allowed, the tester belongs to the campaign, and the campaign is active. Then the bar appears — across the top on desktop, along the bottom on a phone — with the campaign's instructions, the task list with each task's expected outcome, pins already on this page, and the tester's verdict controls. Session recording starts at this point and not before.
- On any other page load — no id, an unknown id, a closed campaign, a disallowed origin — the widget sends the install heartbeat and stays hidden (display:none).
Continue on a phone. The tester's dashboard, the widget and the invitation email can show a QR code that encodes a one-time handoff link (/t/<code>). Scanning it on a phone redeems the code — valid once, for ten minutes — and opens the staging site with the same tester session attached, so mobile tasks are recorded against the same person.
After the seal. When the readiness report is sealed, task results and verdicts are frozen in the widget until the record is reopened; issues can still be filed against the campaign.
What the widget records
Nothing below leaves the browser without an active tester session, except the install heartbeat. URLs pass through a sanitiser that strips tokens, one-time codes and email addresses before they are sent.
Install heartbeat
Every page load with the snippet installed
Site key, the page URL (sanitised) and title, the declared environment label, the build marker and the widget's own version. This is how the dashboard shows the widget as Live, lets you invite testers, and shows Last seen: build X · widget vY on each environment before any tester arrives. It is the only thing sent when no tester session is present.
Page-visit coverage
Active tester session; on load and on every SPA route change
Page URL and title, user agent, and the build marker. The report's page-coverage, device-matrix and effort sections are built from these pings.
Session replay
Active tester session; attached to issues the tester pins
A rolling 60-second window of DOM changes and interactions (rrweb), kept in memory and trimmed to at most 8 MB. When an issue is submitted, the clip leading up to the pin is uploaded with it. Every input value is masked by default — emails, names, card numbers, not just passwords — and any element marked data-ds-mask or .ds-mask has its text masked too.
Annotated screenshot
When the tester pins an issue
A snapshot of the visible viewport at up to 2× device pixel ratio, rendered from the DOM by the browser itself (modern CSS included) with the widget's own UI excluded. The tester can draw boxes and arrows, and redact regions; redactions are burned into the pixels before upload, so the original never leaves the browser.
Element anchor
When the tester pins an issue
Where on the page the pin landed: a ranked set of candidates (a data-ds-id if you set one, then structural selectors, text and position) so the pin can be re-placed on later builds with a confidence score, plus a drafted title from the element it points at.
Ambient context
When the tester submits an issue — shown first as removable chips
The last 50 console errors and warnings, uncaught exceptions and unhandled rejections; the last 25 failed network requests (non-2xx, or a network-level failure) with method, URL, status and duration; and the page's web vitals (LCP, CLS, INP). The widget's own API traffic is never recorded. The tester sees exactly what is attached and can remove any of it before submitting.
Device context
With every issue and task result
Browser and version, operating system, screen size, and the page URL and title. Task results recorded on a phone are marked as such, which is what a Phone-only task checks.
Every uploaded artifact is fingerprinted with SHA-256 on receipt; the fingerprint is what the sealed record and the evidence package carry, so a screenshot or replay can be checked byte-for-byte later. See Verifying a sealed record.
Content-Security-Policy
If your staging site sends a CSP, allow the widget script, the API it calls, and the font it loads inside its shadow root (DM Sans from Bunny Fonts — no Google Fonts, no tracking). Use the hosts from your own install snippet if your deployment is self-hosted.
Content-Security-Policy: script-src … https://cdn.deployseal.com; connect-src … https://api.deployseal.com; style-src … https://fonts.bunny.net; font-src … https://fonts.bunny.net;
Screenshots are rendered from the DOM in the browser, so no img-src change is needed for capture; replay and screenshot uploads go to the API host above.
Ready to install? The snippet with your key lives on the site page.
Open your sitesBuilding on top? See the API reference and the webhook events reference.