Docs / Developer Reference / Documentation

Spectra Developer Documentation

Spectra records what visitors do on your site and keeps the sessions where something went wrong, so you can watch exactly what happened. Recording runs in the background and has no measurable effect on page speed or Core Web Vitals.

60-Second Quickstart

Add the snippet from your Sites page to the head of every page you want recorded. It carries your key, your site id and the address recordings are sent to; the exact values are shown when a key is issued. The recorder initializes immediately and queues commands automatically before network bundle arrival.

HTML / Universal Snippet
<script src="https://api.spectra-trace.com/v1/s.js" data-api-key="sp_live_xxxxxxxx" data-site-id="site_xxxxxxxx" data-endpoint="https://api.spectra-trace.com/v1/ingest" async></script> <!-- Or programmatic initialization: --> <script src="https://api.spectra-trace.com/v1/s.js"></script> <script> Spectra.record({ apiKey: 'sp_live_xxxxxxxx', siteId: 'site_xxxxxxxx', endpoint: 'https://api.spectra-trace.com/v1/ingest' }); </script>

The tag is read for these attributes and no others. A misspelled one is ignored in silence, so if the recorder is not doing what you expect, check the spelling here first.

Attribute Required What it does
data-api-key Yes The key issued for this site. Without it the recorder does not start at all.
data-site-id Yes Which site these recordings belong to. Left out, the key is used as the site id and the sessions are filed under a site that does not exist.
data-endpoint Yes Where recordings are sent. Left out, the recorder guesses the origin the script came from, which is only right while the bundle is served from the ingest host.
data-tunnel No The prefix on your own domain that forwards to Spectra. Set this instead of the endpoint for a first-party install. See Serving Spectra from your own domain.
data-journey-origins No The other origins this visitor's journey crosses, comma separated, for example https://app.example.com. Without it a visit that moves from your marketing site to your application becomes two unrelated recordings. Name them on both sides. See Recording a whole journey.
data-tracing No Present, with no value needed, sends W3C trace headers with requests to this page's own address, so a replay and your server logs name the same request. See Linking a replay to your server logs.
data-tracing-origins No Other addresses that may carry those headers, comma separated, for example your API on https://api.example.com. Those servers must allow traceparent and baggage first, or the requests fail. Naming any address turns tracing on.
data-trace-sample-rate No What fraction of traces your backend is asked to keep, 0 to 1. Defaults to all of them. Turn it down on a busy site if your tracing tool bills by volume; log correlation keeps working at any rate.
data-sample-rate No What fraction of visitors to record, 0 to 1. Records all of them by default. The answer comes from the visitor rather than from a coin toss, so you get that fraction of your visitors with whole journeys, not that fraction of your page views with holes in them, and a visit already under way is always finished. Most sites should leave this alone: an uneventful visit already costs almost nothing, and sampling throws away the visits that turn out to matter.
data-environment No Which deployment this is, for example production or staging. Carried with every recording so a staging session cannot be mistaken for a customer's.
data-block-all-media No Present, with no value needed, replaces every image, video and canvas with a placeholder of the same size. For a site where the pictures themselves are the confidential part and a missed selector would be an incident rather than a gap. The layout is unchanged, so the replay is the real page with the pictures removed. Images referenced from CSS backgrounds are not covered.
data-keyboard No false stops recording the keys that are decisions: Enter, Escape, Tab, the arrows, Backspace, and combinations such as Ctrl+Z. On by default, because without them a replay cannot tell somebody submitting a form from somebody giving up on it, and a menu driven by the keyboard looks like nothing is happening. What a visitor types is never recorded this way on any setting: characters reach a recording only as a field's value, under the masking rules, and a combination pressed in a masked field has its letter replaced too.
data-long-tasks No false stops marking the moments the page stopped responding. On by default. Without them a frozen replay is indistinguishable from a visitor who simply stopped moving. Reported by Chrome and Edge; Safari and Firefox do not measure it, and a recording from those browsers has no such marks.
data-console-levels No Which console levels to record, comma separated, for example debug,log,warn,error. The default is warnings and errors, which is what most sites want: on a busy page the other levels are a running commentary that fills a recording without explaining anything. Uncaught errors and unhandled promise rejections are always recorded whatever you choose.
data-network-bodies No The addresses whose request and response bodies a replay may keep, comma separated, for example /api/,/graphql. Every request is already recorded with its method, address, status, size and timing; this adds the part that says why it failed. Nothing is kept for an address you do not name here. What is kept is redacted first: a value under a key such as password, token or cardNumber is replaced before anything reads it, every remaining piece of text goes through the same scrubbing your page content does, a body that is not text is recorded as its size and type, and there is a limit on each body and on the visit as a whole.
data-network-headers No Which request and response headers may be kept, comma separated, for example content-type,x-request-id. Nothing is kept unless you name it. Authorization, Cookie and the other credential headers are refused even when named, and a header whose own name says it carries a secret is replaced rather than kept.
data-network-deny-keys No Extra field names to treat as secrets, comma separated. The built-in list covers the usual ones; use this for the fields only you would recognise, for example an internal reference that identifies a person.
data-network-sockets No false stops recording WebSocket, server-sent event and beacon activity. On by default, and it records only the connection and how much went each way, never what was said over it. Without it a replay of a page that does its work over a socket shows an empty network list under a screen that is plainly updating.
data-release No The build this page came from, for example 2026.09.11. Sessions and issues are grouped by it, which is how you tell whether a problem arrived with a deploy. This is the one place a templating layer can write it for a script-tag install.
data-consent No true or false. Say false when your consent banner has not been answered yet, and nothing is recorded until it is. Leave the attribute off entirely and the recorder behaves as it always has, so adding it is a decision rather than a default. To answer later, from the banner itself, call spectraQ.push(['consent', true]).
data-respect-dnt No Present, with no value needed, honours Do Not Track and Global Privacy Control and records nothing for visitors who send either. Off by default on purpose: Brave sends GPC on every request and Firefox private windows send DNT, so switching this on will stop recording a real share of your visitors. That is the right trade for some sites and the wrong one for others, which is why it is yours to make.
data-persistence No local (the default), session, or memory. How long anything the recorder writes may stay on a visitor's device. Consent is required for storing something on a device, not for recording a visit, so session lets you keep recording when your banner has been refused. It keeps nothing past the visit: the visitor id moves to sessionStorage and undelivered data is held in the page. Sessions still join across the pages of a visit, journeys still work, and a refused upload is still retried while the page is open. What you give up is returning visitors, who arrive as new ones, and an upload still waiting when the tab closes, which is lost rather than sent later. memory writes nothing at all and every page becomes its own recording, which also means every page counts as a separate session against your plan. That makes session the one to reach for unless you have a reason not to. Cross-domain journeys are unaffected by any of these, because they travel in the link rather than in storage.
data-ignore-bots No Present, with no value needed, records automated browsers too. The recorder refuses them by default, which is almost always what you want; turn this on when you are testing your own install with a headless browser and wondering why nothing appears.
data-debug No Present, with no value needed, makes the recorder narrate what it is doing to the browser console. Use it while installing, then take it off. It is the only way a script-tag install can ask the recorder what it decided.
Async Load Queue: For Google Tag Manager or custom script loaders, commands can be pushed to window.spectraQ prior to script execution without risking execution order errors.

After the snippet: what to set up

The snippet on its own records visits, finds the moments people struggled, and keeps the sessions worth keeping. Everything below is optional, and each one answers a question you will eventually ask of a recording. They are in the order most teams want them.

1. Say who the visitor is

Without this every session belongs to an anonymous visitor, and a support request that starts with a customer name has nowhere to go. One call, at sign-in:

Spectra.current()?.identify(user.id);

It applies to the whole session including everything before the call, so signing in halfway through a visit still attaches the part that led up to it. Their previous visits group under the same person too. In the session list, clicking a visitor shows everything else they did.

2. Say which build and which deployment

Two attributes your build writes, and the reason to bother with them is the question "did this start with the last deploy", which nothing else can answer.

Script tag
<script src="…/v1/s.js" data-api-key="…"
        data-release="2026.09.25"
        data-environment="production"></script>

The Issues page then groups problems by build and tells you which one a problem arrived in and whether it is still there. The environment keeps your staging visits from being read as customers: set it on every deployment, not only production, or the untagged ones are the ambiguous ones.

3. Report the errors your own code catches

Uncaught errors are recorded for you. An error your framework catches never reaches the browser, so nothing outside your code can see it, and the better your error handling the less a replay knows. One line puts them back. See Reporting errors your code catches for React, Vue, Angular and script-tag examples.

4. Say where to be told when something breaks

Under each site, in Tell me when it breaks, paste a Slack incoming webhook or any address of your own. Without it, a page that stops working is found immediately and mentioned whenever somebody next opens the dashboard, which at three in the morning is not much use. See Being told when something breaks.

5. Decide what a replay may keep of your traffic

Every request your page makes is already recorded with its address, status, how long it took and what asked for it, and so is everything the page loaded. What is not kept, until you name the addresses, is what the requests actually said:

networkCapture: {
  bodies: { urls: ['/api/'] },
  headers: ['content-type', 'x-request-id'],
}

That is the difference between "it returned 400" and "it returned 400 because the postcode was empty". Nothing is kept for an address you did not name, credential headers are refused even if you list them, and values under names like password or cardNumber are replaced before anything reads them. Start with one address and widen it.

6. Mark anything that must never be recorded

Input values are masked by default and passwords always are. For a region of the page, add data-spectra-block to the element and it is replaced by a placeholder of the same size. For a screen you cannot mark up in advance, such as a document another customer uploaded, pause instead:

Spectra.current()?.pause();   // before showing it
Spectra.current()?.resume();  // after

The session carries on either way. If the pictures themselves are the confidential part and a missed selector would be a problem rather than a gap, use data-block-all-media and every image, video and canvas becomes a placeholder. See Privacy and masking.

What you do not have to do

Page loads, scripts, stylesheets, fonts and images are recorded without any configuration, as are clicks, typing, scrolling, the keys that are decisions such as Enter and Escape, how fast each page was for that visitor, and the moments the page stopped responding. Rage clicks, dead clicks, form trouble, failed requests and stuck loading screens are detected for you. None of it needs an option set.

Verifying Installation

Confirm telemetry transport directly from your browser developer tools console. Run the following command on any page where the SDK is embedded:

Browser Console Verification
Spectra.current().flag('test_install');

The Spectra Sessions dashboard updates within seconds displaying a test_install trigger tag alongside your current session replay.

Serving Spectra from your own domain

Content blockers refuse requests to third-party hosts that appear on a filter list. Served from Spectra's host, the recorder is third-party to your site. Served from your own domain, there is nothing to refuse: the script, its configuration and every recording travel on your origin under a path that says nothing about what it is. This is the install the Sites page recommends, and it needs one forwarding rule on your host.

Your own Content-Security-Policy is the second reason, and on measurement it is the larger one. We ran the recorder against forty public sites: seventeen of them refused the direct install at connect-src before it could fetch its configuration, so it started, threw nothing, and recorded nothing. Nothing on your page would have told you. Served from your own origin the recorder is same-origin, which connect-src 'self' already permits, so a policy you have not changed keeps working.

If you would rather install directly and your site sets a policy, allow https://api.spectra-trace.com in both script-src and connect-src. The recorder now says so in your browser console when a policy blocks it, once per page, naming the directive that refused it.

How it works

Your host forwards one prefix, for example /q4hx/*, to https://api.spectra-trace.com/v1/*. The Sites page shows the tag with your key and a prefix chosen for your site; any prefix works as long as the tag and the rule agree.

Every rule below forwards the whole prefix, which is what we recommend. If your host or firewall needs the paths listed instead, these are all of them. A path your proxy does not forward is lost with no error on your page and nothing on ours, so the list has to be complete:

Path What it carries
/s.jsThe recorder itself.
/iRecordings.
/cThis site's configuration.
/pOne small anonymous count per page.
/uThe per-visit analytics summary.
/usThe page snapshot a heatmap is drawn on.
/aStylesheets, stored once per version and shared by every recording that uses them.
/mImages and fonts, so a replay looks like the page did.

The tag
<script src="/q4hx/s.js" data-api-key="sp_live_xxxxxxxx" data-site-id="site_xxxxxxxx" data-tunnel="/q4hx" async></script>

The rule, per host

Cloudflare Pages (_redirects)
/q4hx/* https://api.spectra-trace.com/v1/:splat 200
Netlify (netlify.toml)
[[redirects]] from = "/q4hx/*" to = "https://api.spectra-trace.com/v1/:splat" status = 200 force = true
Vercel (vercel.json)
{ "rewrites": [{ "source": "/q4hx/:path*", "destination": "https://api.spectra-trace.com/v1/:path*" }] }
nginx
location /q4hx/ { proxy_pass https://api.spectra-trace.com/v1/; proxy_set_header Host api.spectra-trace.com; }
Next.js (next.config.js)
async rewrites() { return [{ source: '/q4hx/:path*', destination: 'https://api.spectra-trace.com/v1/:path*' }]; }
Express, with http-proxy-middleware
app.use('/q4hx', createProxyMiddleware({ target: 'https://api.spectra-trace.com', changeOrigin: true, pathRewrite: { '^/q4hx': '/v1' }, }));
Two things the proxy must do, and every one above does by default: forward the request headers, because recordings identify themselves with headers beginning X-Spectra- and a proxy that forwards only the content type looks like it works and records nothing; and forward the method and body unchanged.

Recording a whole journey

Do you need this?

If your whole site sits at one address, like example.com, then no. Paste the snippet and you are finished. A visit that moves through ten of your pages is already one recording.

You need this page if your visitors move between two different addresses that both belong to you. A marketing site at www.example.com and an app at app.example.com is the usual shape. Without the setting below, somebody who reads your pricing page and then signs up becomes two separate recordings, and the interesting part is split down the middle.

The rule that matters most

Spectra can only record a page that has the snippet on it. There is no setting that lets one page record another. If a page does not have the snippet, that page produces nothing at all, and nothing anywhere reports it.

So put the snippet on every page you want to watch, at every address. In practice that means putting it in the shared layout or header template all your pages are built from, once, so nobody has to remember. If your two sites are separate projects, each one needs it in its own layout.

The setting below does a different job. It does not reach other pages. It tells two snippets that are already running that they are watching the same visit, so their two recordings become one.

Joining two of your addresses

List your addresses in the tag, and use the same tag on every page of both sites:

The same on every page, on both sites
<script src="https://api.spectra-trace.com/v1/s.js" data-api-key="sp_live_xxxxxxxx" data-site-id="site_xxxxxxxx" data-endpoint="https://api.spectra-trace.com/v1/ingest" data-journey-origins="https://www.example.com, https://app.example.com" async></script>

Two things to get right, and that is all of it:

List every address, including the one the page is on. The simplest thing is to paste the identical tag everywhere rather than tailoring the list per site. An address you have not listed is never given a session and never accepts one, so listing only one side leaves you exactly where you started: two recordings, nothing broken.

Use the same data-site-id on both. A recording is filed under its site and its session together, so two different site ids mean two recordings no matter what else is true.

What this covers

People leave a page in more than one way, and something that only handled links would miss most of them:

How the visitor moves Between your subdomains To another domain of yours
Clicks a link, or submits a form Yes Yes
Your server redirects them, after signing in Yes With one header, below
Types the address, or follows a link in an email Yes No
Your own code sets location.href Yes With one line, below

About the cookie, and visitors who refuse it

Between subdomains of one site, a small cookie carries the session. It is the only thing that survives a redirect made by your own server, because the browser sends it whatever caused the visitor to move.

If you list no addresses, no cookie is written at all. That is the ordinary install, and you can tell a privacy reviewer plainly that the product sets no cookie. When you do list them, the cookie holds a reference to the session and nothing about the person, has no expiry date, and disappears when the browser closes. It is a handover between two pages of one visit, not a record of who somebody is.

If a visitor blocks or refuses cookies, nothing breaks. Moving through one address keeps working, because that never used a cookie. Clicking a link or submitting a form to your other address keeps working, because the session travels on the link itself. Only the two cases that need the cookie fall back, and they fall back to what you had before: two recordings instead of one, with no errors and nothing for the visitor to notice.

This is not the same as consent to record. If a visitor has refused to be recorded, that is the hasConsent option, and it stops recording altogether. This section is only about somebody who allows recording but blocks cookies.

A redirect your own code makes

There is one move nothing can see: your code setting location.href. If it goes to another domain of yours, hand the session over in one line:

Carrying the session by hand
window.location.href = window.Spectra.current().handoff(checkoutUrl);

An address you did not list comes back unchanged, so this is safe to put before any redirect, including one to a payment page. When the destination is somewhere no recording is possible, it also marks the moment in the replay, which is worth doing on its own.

A redirect your server makes, between two of your domains

This is the last case, and the only one that needs anything from your backend. Between subdomains it is already handled and you can skip this. It applies when your server decides to send the visitor from one of your domains to a genuinely different one, for example after a sign-in.

Nothing in the browser can carry that on its own. There is no link to attach anything to, because the move was decided on your server after the page was gone, and a cookie cannot cross from one domain to another. The only thing in a position to carry it is the server issuing the redirect. So give it something to carry:

On the page, when you call your backend
fetch('/login', { method: 'POST', headers: { 'X-Journey-Token': window.Spectra.current().journeyToken() }, body: form, });
On your server, on the redirect it sends back
Location: https://shop.example.com/checkout?_spz=<the value of that header>

The arriving page reads it and carries on as the same recording. The header name is yours to choose; only the _spz parameter matters, and it is removed from the address before the page records anything.

What is in that token. A reference to the session, a counter, and a timestamp. No API key, no user id, no personal data of any kind, so it is safe in your server logs. It stops being accepted after one minute, and it is only ever accepted at an address you listed. If you pass it somewhere you did not list, nothing happens at all.

Why this one is not automatic, when everything else is: a cross-domain redirect happens entirely on your server. The browser is told where to go and goes, and no page of yours is involved to carry anything. Every session replay product has this limit. Most do not offer a way through it at all.

Sites you do not own

This is the distinction that matters, and it is about whose page it is, not which address it sits at. Your second domain works because you can put the snippet on it. A payment page at Stripe or Dodo, or a sign-in page at an identity provider, is somebody else's site: you cannot put a script there, so nobody can record it. Not us, and not any other session replay product either.

What you get instead is the shape of the absence. The replay shows left for checkout.stripe.com, then returned after 3m 12s, and the visit carries on as the same recording. What happened before the payment and what happened after it stay one story rather than becoming two.

If journeys still split, check these in order. Does every page carry the snippet, including the one the visitor landed on second? Does each site list the other's address as well as its own? Do both use the same site id? If all three are right and only server-side redirects between two different domains are splitting, that is a real limit rather than a mistake: there is no link to carry anything, and a cookie cannot cross domains. Sending the header Referrer-Policy: no-referrer also turns this off between separate domains, because the arriving page is then told nothing about where the visitor came from.

Linking a replay to your server logs

A replay stops at the edge of the browser. It shows you that a request took nine hundred milliseconds and came back a 500; it cannot show you what your server did inside it. So the person debugging has the replay open in one window and their logging or tracing tool in the other, trying to match a timestamp against a list of thousands, for a request that happened four hours ago.

Turn this on and both sides give the request the same name, so you can go straight from one to the other.

Same-origin requests
record({ apiKey: 'sp_live_xxxxxxxx', siteId: 'site_xxxxxxxx', tracing: true, });

Every request your page makes to its own address now carries two extra headers:

Header What it is
traceparent The W3C standard for naming a request across systems. If you run OpenTelemetry, Datadog, Jaeger, Honeycomb or anything else that speaks it, your server already understands this header and will hang its own work underneath it. Nothing to configure on your side.
baggage Carries spectra.session=<id>. If you have no tracing tool at all, log this one value and every line of your server log can say which replay it belongs to.

The replay records the same trace id beside each request, so a viewer has something to paste into your tracing tool.

Requests to a different address

If your API lives somewhere else, name it:

Naming an API on another address
record({ apiKey: 'sp_live_xxxxxxxx', siteId: 'site_xxxxxxxx', tracing: { origins: ['https://api.example.com'] }, });
Your API has to allow the headers. Sending an unexpected header to a different address makes the browser ask permission first, and a server that does not answer correctly fails the whole request. So add traceparent and baggage to your Access-Control-Allow-Headers before you name an address here. This is why it is never done for you: a missing link between two tools costs you a search, and a broken API call costs you an outage.

Requests to your own address are safe without any of that, which is why tracing: true covers them and nothing else.

Keeping your tracing bill and your metrics sane

Two things are worth setting deliberately before you turn this on across a busy site.

How many traces your backend is asked to keep. The traceparent header carries an instruction: keep this one. By default every request carries it, so correlation works without tuning, but that also means a tracing tool that charges by volume will keep everything. If you have sampling turned down, turn this down to match:

Marking one trace in twenty
record({ apiKey: 'sp_live_xxxxxxxx', siteId: 'site_xxxxxxxx', tracing: { sampleRate: 0.05 }, });

Turning it down does not turn off the link. The headers still go out on every request; the trace is simply marked as one your backend may drop. Since logs are usually not sampled at all, correlating a log line with a replay keeps working for every single request, whatever rate you pick.

Where the session id is allowed to end up. The session id is unique per visit, which is exactly what makes it useful for correlation and exactly what makes it dangerous as a metric dimension: a metric split by it becomes one series per visitor, which is how monitoring bills get out of hand.

By default there is nothing to worry about. OpenTelemetry does not copy baggage onto spans unless you have added the baggage span processor yourself, and nothing copies it onto metrics. If you have added that processor, or you generate metrics from span attributes, do one of these:

Keep it off your metrics
# If you use the baggage span processor, exclude it: BaggageSpanProcessor(excluded=["spectra.session"]) # Or, if you build metrics from span attributes, leave it out # of the dimensions. It belongs on traces and logs, not metrics.

On a trace or a log line it costs nothing: both are stored per event and looked up by id, which is the whole point of putting it there. It is only aggregation that cannot take a value unique to every visitor.

On OTLP. Spectra does not send anything over OTLP, and does not need to. OTLP is how spans and metrics are exported to a collector; what makes a replay and a trace line up is propagation, which is the traceparent header and a separate standard. Your existing tracing keeps exporting over OTLP exactly as it does today, and its spans now carry an id the replay also knows. If you want Spectra's own events inside your collector rather than in our dashboard, that is a different thing and we do not do it yet.

Things it will not do

It will not disturb tracing you already have. If your page already sends traceparent, that one is left exactly as it is and we record its id as the one to search for. If you already use baggage, ours is added to yours rather than replacing it.

Nothing is sent unless you ask. With no tracing setting, no request is touched at all.

Spectra is not a tracing tool. We do not collect spans or show you a flame graph, and we are not trying to replace what you already run. All this does is make sure both systems call the request by the same name.

Nothing personal travels in these headers. A trace id, a request id and the session id. No account details, no API key.

What does not work

A DNS alias is not first-party. Pointing a subdomain of yours at Spectra's host with a CNAME does not help: uBlock Origin on Firefox resolves the alias and blocks the request anyway. The forwarding has to happen on your own host.

Some visitors cannot be recorded by anyone. A visitor with JavaScript disabled, or a blocker set to refuse every script, does not run the recorder at all. No install changes that.

Content security policy and bundled apps

If your site sends a Content-Security-Policy, add worker-src blob: whichever install you chose. The recorder compresses recordings on a worker it builds from a blob: URL, and worker-src falls back to script-src when it is absent, so a policy of script-src 'self' refuses it. Nothing breaks and nothing is reported: the recorder compresses on your visitor's own thread instead, on the page whose responsiveness you installed us to measure.

Beyond that, a first-party install needs nothing added, because everything else it asks for is 'self'. The direct install also needs https://api.spectra-trace.com in script-src and connect-src.

First-party install
Content-Security-Policy: script-src 'self'; connect-src 'self'; worker-src 'self' blob:;
Direct install
Content-Security-Policy: script-src 'self' https://api.spectra-trace.com; connect-src 'self' https://api.spectra-trace.com; worker-src 'self' blob:;

These are the directives Spectra needs; keep whatever else your policy already sends.

If you bundle the recorder rather than loading the tag, pass the prefix as the tunnel option and it is used for every request:

Bundled
import { record } from '@spectra/recorder'; record({ apiKey: 'sp_live_xxxxxxxx', siteId: 'site_xxxxxxxx', tunnel: '/q4hx' });

A tunnel value that already carries a query string keeps the older ?target= form, so proxies written against it keep working. New installs should use the prefix: one rule instead of a hand-written proxy, and nothing for a filter to match.

Recording rules: what gets recorded

Spectra does not keep every session. A session is kept when something worth seeing happens: an error, a sign of frustration, or a moment your own rules mark as important. Everything else is discarded, which is why your quota goes a long way.

Automatic Default Triggers

Shown as What happened Code
JS error An uncaught JavaScript error. uncaught_error
Unhandled rejection A promise rejected with nothing to catch it. unhandled_rejection
Console error A console.error() call. console_error
Failed request A request answered with a 4xx or 5xx status. network_error
Slow request A request somebody pressed a button for and then waited ten seconds or more to be answered. Only requests that began while somebody was waiting count, so long-polling, uploads and background work are not mistaken for a wait. slow_request
Unreachable request A request that never completed at all: refused by the browser as cross-origin, a connection closed, a name that would not resolve, a certificate rejected, or the network gone. Requests your own code cancelled are not counted, and neither are those cut short by the visitor leaving. network_unreachable
Resource failed to load A script or stylesheet the page asked for and did not get, which is what a bad deploy looks like from the browser. Images and media that fail are recorded too, and shown on the recording, but do not start one on their own. resource_load_error
Blocked by your policy Your Content Security Policy refused something the page tried to load or run. The browser writes this to its own console, where no script can read it, so it is heard from the policy event instead. csp_violation
Rage click Several rapid clicks on the same spot that changed nothing. rage_click
Dead click A click on something that looks interactive and is not. dead_click
Form error A form submitted and answered with a validation error. form_error
Form thrashing A field cleared and retyped repeatedly. form_thrash
Abandoned after effort A visitor left after sustained interaction with no completion. abandoned_after_effort
Stuck loading A page that stayed in a loading state past the threshold. stuck_loading
Struggle A run of the friction signals above in one short window. struggle
Install test The check you run from the browser console to confirm the install. test_install

Programmatic Code Triggers

Dispatch custom business events or update session properties dynamically from your application logic:

SDK Event Signals
const spectra = Spectra.record({ apiKey, siteId, endpoint }); // 1. Emit business event for rule evaluation spectra.track('PaymentFailed', { gateway: 'stripe', errorCode: 'card_declined' }); // 2. Set session metadata properties spectra.setProps({ userPlan: 'enterprise', cartValue: 499 }); // 3. Force-record session immediately with custom reason label spectra.flag('vip_checkout_stuck');

Zero-JS HTML Markup Triggers

Annotate HTML elements directly to capture friction points without custom script logic:

HTML Trigger Attributes
<button data-spectra-trigger="checkout_submit_clicked">Complete Purchase</button> <div data-spectra-flag="card_authorization_failed">Your card was declined</div>

Dashboard Trigger Rule Conditions

Configure dynamic trigger rules in the Spectra dashboard, under Sites and then Trigger Rules. Rules propagate instantly to client SDKs without requiring code redeployments:

Trigger Rule JSON Schema
[ { "id": "high-value-payment-failed", "on": { "event": "PaymentFailed" }, "where": [{ "key": "cartValue", "op": "gte", "value": 300 }], "reason": "high_value_payment_failed", "sampleRate": 1.0, "kind": "issue" } ]
Operator (op) Description
eq / neq Equals / Not equals target value.
gt / gte Greater than / Greater than or equal to numeric threshold.
lt / lte Less than / Less than or equal to numeric threshold.
contains String substring matching.
exists Property key presence check.

Framework Integration Reference

React and Next.js (App Router)

Install the official package from npm:

Terminal
npm install @spectra/recorder
components/SpectraProvider.tsx
'use client'; import { useEffect } from 'react'; import { record } from '@spectra/recorder'; export function SpectraProvider() { useEffect(() => { const instance = record({ apiKey: process.env.NEXT_PUBLIC_SPECTRA_KEY!, siteId: process.env.NEXT_PUBLIC_SPECTRA_SITE_ID!, endpoint: 'https://api.spectra-trace.com/v1/ingest', }); return () => instance.stop(); }, []); return null; }

Vue 3 and Nuxt 3

Create a client plugin inside your Nuxt project under plugins/spectra.client.ts:

plugins/spectra.client.ts
import { record } from '@spectra/recorder'; export default defineNuxtPlugin(() => { record({ apiKey: 'sp_live_xxxxxxxx', siteId: 'site_xxxxxxxx', endpoint: 'https://api.spectra-trace.com/v1/ingest', }); });

SDK Configuration Parameters

The record() initialization method accepts an object with the following parameters:

Parameter Type Default Description
apiKey string Required The key issued for this site. Shown once when issued; rotate it from the Sites page.
siteId string Required Unique site identifier linking recorded sessions to your organization workspace.
endpoint string Required Where recordings are sent. Shown with your snippet on the Sites page.
tunnel string null A path on your own site that forwards to Spectra, so recording is not affected by content blockers. See Ad-Blocker Tunneling.
tracing boolean | object off Send W3C traceparent and baggage headers so a replay and your server logs name the same request. true covers your own address; { origins: [...] } adds others, which must allow the headers. See Linking a replay to your server logs.
journeyOrigins string[] [] The other origins your visitors' journeys cross. Named on both sides, a visit that moves between them stays one recording. Nothing is ever carried to an origin not in this list.
userId string null Internal user identification string for cross-session correlation.
privacyMode string 'mask-pii' One of 'strict', 'mask-pii', 'mask-inputs' or 'allow'. The default masks every input value and scrubs recognisable personal data out of page text. 'strict' redacts all plain text across the document tree.
maskInputs boolean true Automatically masks input element values before recording.
respectDoNotTrack boolean false Stop recording a visitor whose browser sends Do Not Track or Global Privacy Control. Off by default because Brave sends Global Privacy Control for every visitor and Firefox sends Do Not Track in private windows, so turning it on for you would quietly remove a large share of your traffic from your own recordings. Turn it on when that is what you want.
hasConsent boolean or function None A consent gate. Recording waits until this is true, so a consent banner can hold it back. Pass a function to have it checked when recording starts. Nothing is written to the visitor's browser while it is false, including the anonymous visitor id.
pulse boolean true Send a small anonymous count of page views and interactions per page, used to detect a page whose conversion has dropped. No content, no identity.
analytics boolean true Measure behaviour on this site: where people click, how far they scroll, what frustrates them and how fast each page is. Only ever collected for a workspace that has the UX analytics add-on; set false to switch it off for one site that does.
frameBridge boolean | { parentOrigins: string[] } true On a page, let iframes on the same key record into its session. Inside a frame, name the pages allowed to adopt it, for example { parentOrigins: ['https://app.example.com'] }; a frame that names none sends nothing to whatever framed it. Inputs inside frames are always masked.
requireAuth boolean false Record only when a userId is set; anonymous visitors are not recorded.
securityKey string undefined A second value that must be given, and must equal your apiKey, before recording starts. Anything else stops recording silently, so set it only if you have a reason to require the key twice.
customFilter function undefined A function receiving the configuration and returning whether to record this visit.
ignoreBots boolean false Record automated browsers too. By default crawlers and headless browsers are skipped.
idleThresholdMs number 3000 How long with no activity before the visitor is considered idle. Idle time is skipped in playback.
flushIntervalMs number 5000 How often queued activity is uploaded while a session is being kept.
smartRecording object enabled Keep only sessions where something happened. Pass { enabled: false } to keep every session from page load, within your plan's limits.
triggerRules array [] Rules evaluated against track() and setProps(). Rules saved in the dashboard are merged in automatically.
onError function undefined Called with any error the recorder encounters. Recording never throws into your page.
release string None The build this page came from, for example 2026.09.25. Sessions and issues group by it, which is how you tell whether a problem arrived with a deploy.
environment string None Which deployment this is: production, staging, a review app. Filterable in the session list, so a staging visit is never read as a customer.
sampleRate number 1 What fraction of visitors to record, 0 to 1. Decided from the visitor rather than a coin toss, so you get that fraction of visitors with whole journeys rather than that fraction of page views with holes, and a visit already under way is always finished. Most sites should leave this alone: an uneventful visit already costs almost nothing.
networkCapture object metadata only What a replay may keep about your requests beyond the address, status and timing it already records. { bodies: { urls: [...] }, headers: [...], denyKeys: [...], sockets: true }. Nothing is kept for an address you do not name. See After the snippet: what to set up.
consoleLevels string[] ['warn', 'error'] Which console levels are recorded. Add log, info or debug when your own diagnostics go through them. Uncaught errors and unhandled rejections are always recorded whatever you choose.
keyboard boolean true Records the keys that are decisions: Enter, Escape, Tab, the arrows, and combinations such as Ctrl+Z. What a visitor types is never recorded this way on any setting; characters reach a recording only as a field value, under the masking rules.
longTasks boolean true Marks the moments the page stopped responding. Without them a frozen replay is indistinguishable from a visitor who stopped moving. Measured by Chrome and Edge; Safari and Firefox do not report it.
blockAllMedia boolean false Replaces every image, video and canvas with a placeholder of the same size, for a site where the pictures themselves are confidential. The layout is unchanged. Images referenced from CSS backgrounds are not covered.
persistence 'local' | 'session' | 'memory' 'local' How long anything the recorder writes may stay on a visitor's device. session keeps nothing past the visit, which lets you keep recording when a consent banner has been refused. memory writes nothing at all, and makes every page its own session.
dashboardUrl string Spectra Where getSessionUrl() points. Only needed for a self-hosted deployment.
slowRequestMs number 10000 How long a request somebody is waiting on may take before the session is kept. Only counts requests that started while a visitor was waiting, so background polling does not flag every visit.
stuckLoadingMs number 15000 How long a loading indicator may stay on screen before the session is kept as a stuck loading screen.
debug boolean false Prints what the recorder is doing to the console. For checking an install; not for production.
onDiagnostic function undefined Called with what the recorder decided and why, as it happens. The programmatic form of debug.
blockClass string | RegExp 'spectra-block' A class name that replaces an element, and everything inside it, with an empty box of the same size. A string matches a class exactly, so secret matches class="secret" and not class="secret-note"; a regular expression is tested against each class on the element. The default already works, so you can hide an element today by giving it class="spectra-block". The box keeps the size the page was laid out around, and for a table cell or a dropdown option it keeps the few attributes that hold the layout together, including that option's value.
blockSelector string | null null The same as blockClass, written as a CSS selector, for hiding something you cannot add a class to. Checked once when recording starts: a selector this browser cannot parse is ignored and said once in the console, and everything else you asked to hide still is. That is the one place a mistake here hides nothing rather than too much, so check the console when you set it.
maskTextClass string | RegExp 'spectra-mask' A class name that replaces the text inside an element with mask characters, while leaving the element itself in the recording. Put it on a container and every piece of text beneath it is masked. A string matches a class exactly, and a class holding a slash or a colon is safe. The default already works, so class="spectra-mask" masks text today.
maskTextSelector string | null null The same as maskTextClass, written as a CSS selector. It matches the element or any element above it, so a selector naming a container masks everything inside. Validated when recording starts, with the same console warning as blockSelector.
maskChar string '*' The character used when something is masked and you have not supplied a function to do it. One per character of the original, so the length of a hidden value is still visible in the recording. An email address and an authorisation token are replaced with a fixed run instead, so their length is not. Keep it to a single character: a longer string is repeated per character and doubles the length of every masked value.
maskInputFn function one mask character per visible character Called with a field's value and its element, once per value, and what it returns is what the recording keeps. It is called for the fields that are masked outright. It is deliberately not called for an email or telephone field, or one your markup marks as a card number: those are replaced with a fixed run so their length says nothing either. Returning the value unchanged turns masking off for every field it covers.
maskTextFn function one mask character per visible character Called with the text of each masked text node and its parent element. Per text node, not per element, so a sentence a framework has split into three pieces arrives in three calls and your function cannot see the whole of it. When you set this, maskChar no longer applies to text.
scrubPII boolean true Finds and masks personal detail in text the recording would otherwise keep as written: card numbers that pass a checksum, so order numbers and product codes survive; social security numbers in the United States format; email addresses; telephone numbers in the North American format; and authorisation tokens. It covers text and the values of fields. It does not cover attributes, so a title, alt, placeholder or aria-label is kept as written, and it does not know names, postal addresses or account numbers. Use customPIIRegexes, a class or a selector for those.
customPIIRegexes RegExp[] [] Your own patterns, masked wherever they appear in text, after the built-in ones. Give each one the g flag: without it only the first match in a piece of text is masked. These run on every piece of text the recorder keeps, so a pattern that backtracks badly costs the visitor's browser. Turning scrubPII off turns these off with it.
recordCanvas boolean true Records what a two-dimensional canvas is showing, so charts, maps and signature pads appear in the replay rather than as an empty rectangle. Turn it off for a canvas whose contents are confidential, or hide it with blockClass.
recordWebGL boolean false Records three-dimensional canvases as well. Off by default because reading one back costs the visitor's browser more than a flat canvas, and because a page can be built so that reading it back is not possible at all. A three-dimensional scene is an empty rectangle in the replay until you turn this on.
inlineStylesheets boolean true Keeps the text of a stylesheet the page loaded, so the replay looks like the page did even after you deploy a new one. Turn it off and the replay asks your own site for the stylesheet when somebody watches, which looks wrong once that file has changed or moved.
keepIframeSrc function keeps no frame addresses Called with the address of each frame on the page. Return true to keep that address in the recording, so the replay loads the frame live. By default no frame address is kept, because loading a third party's frame at watch time shows whatever it shows today rather than what the visitor saw.
userDedupeWindowMs number 0 A cooling-off window, in milliseconds, for one signed-in person. Zero turns it off. Set it and the same userId is recorded at most maxSessionsPerWindow times within the window, which is for a page somebody reloads repeatedly.
maxSessionsPerWindow number 3 How many recordings one signed-in person may produce inside userDedupeWindowMs. Ignored while that window is zero.
smartRecording.frictionDetection boolean true Whether a recording is kept when a visitor clicks something repeatedly, clicks something that does nothing, or is shown a form error. Turn it off to stop those from being reasons on their own.
smartRecording.struggleDetection boolean true Whether a recording is kept when a visitor tries the same thing several ways, or puts real effort in and then leaves without finishing.
smartRecording.loadingDetection boolean true Whether a recording is kept when a loading indicator stays on screen longer than stuckLoadingMs.

User Identity and Event Tracking

Link incoming replay recordings to your internal user accounts using identify():

User Identification
const spectra = Spectra.record({ apiKey, siteId, endpoint }); // Call after user login completes spectra.identify('user_94821');

What the recorder returns

Spectra.record() returns an instance, also reachable as Spectra.current(), with these methods:

Method What it does
identify(userId)Attach the whole session, including everything before this call, to one of your user ids. Pass a string; the first 128 characters are kept.
track(name, props)Record a business event. It appears in the replay's event list and is evaluated against your rules.
setProps(props)Set properties of the session such as plan or cart value. Rules watching a property re-evaluate when it changes.
flag(reason, details)Keep this session now, under a reason you name. triggerRecording is the same call.
emitCustomEvent(tag, payload)Add a custom marker to the timeline.
pause() / resume()Stop watching the page without ending the session, for a screen a recording must not contain. Everything recorded before the pause still arrives, the visit stays one visit, and nothing about the page is observed while paused. Resuming photographs the page again.
getSessionUrl()Where to watch this session, for your own logs and tickets. Null when nothing is being recorded.
captureError(error, context)Report an error your own code caught. Uncaught errors and unhandled promise rejections are recorded automatically; an error your framework catches never reaches the browser, so this is how it reaches the replay. See Reporting errors your code catches.
takeFullSnapshot()Capture the page again from scratch, for pages that rebuild their whole DOM.
stop()Stop recording for this page.
sessionIdThe id of the current session, for your own logs.

Reporting errors your code catches

Uncaught errors and unhandled promise rejections are recorded for you, and the session they happened in is kept. But a framework catches: that is what an error boundary is for, and an error it catches never reaches the browser, so nothing outside your code can see it. The better your application handles its failures, the less a replay knows about them. One line puts them back.

React
class Boundary extends React.Component {
  componentDidCatch(error, info) {
    Spectra.current()?.captureError(error, { component: info.componentStack });
  }
  render() { return this.props.children; }
}
Vue
app.config.errorHandler = (error, instance, info) => {
  Spectra.current()?.captureError(error, { where: info });
};
Angular
@Injectable()
export class SpectraErrorHandler implements ErrorHandler {
  handleError(error: unknown) {
    Spectra.current()?.captureError(error);
  }
}
A script tag, with no bundler
window.spectraQ = window.spectraQ || [];
try { risky(); } catch (error) {
  window.spectraQ.push(['captureError', error, { where: 'checkout' }]);
}

The error is recorded with its message, its stack and any properties you attached to it, so a tagged error keeps the status code that explains it. The session is kept for review, and the message is scrubbed for personal data exactly like every other message.

Sessions and filters

Sessions lists every recording kept for the selected site, newest first, with why it was kept. Filter by the reason, by one of your user ids, by a release or environment, or by a page URL, which matches anywhere the visit went rather than only where it started. The second row of filters narrows by what they were on and how the visit went: phone, tablet or desktop, browser, operating system, how long they stayed, and whether anything broke. Clicking a visitor shows everything else that person did. A site with no recordings yet shows the install steps in place until the first one arrives.

Issues

Issues groups recordings by what went wrong, so the same error on the same page is one row however many people hit it. Open a row to see the pages affected and the recordings behind it. Choose the window at the top: the last day, week or month.

Analytics

Sessions and Issues answer questions about one visit at a time. Analytics answers the ones that only exist across many: which control nobody finds, how far down a page people actually get, which page quietly loses them. It measures every visit rather than the share worth recording in full, so a page with no recordings still has numbers.

Overview is the site at a glance for the window you choose: how many visits, how long they lasted, how much of that was active, and how many ran into something that went wrong. It also carries the three speed measures a visitor actually experiences.

Pages ranks every route by traffic, with time on page, how far people scrolled, and how many arrived from elsewhere on your site and were gone within five seconds. Open a row for that page on its own.

The heatmap is drawn over your real page rather than a screenshot. A masked capture of the page is rebuilt in the browser and the counts are attached to the elements themselves, so the map stays correct when your layout changes and can be shown at desktop, tablet or phone width. Switch between clicks, rage clicks, dead clicks and clicks that caused an error. Each busy element wears its share of the page's clicks in the view you are on, the rail beside the picture ranks the busiest and explains the colours, and hovering a row lights its element on the picture. Click any element to list the visits that clicked it, with replays first.

Scroll draws how far down the page visits got, as bands over the page itself: the share of visits that reached each part, labelled every quarter, and a dashed line where a typical first screen ended. Anything that matters wants to sit above that line.

The picture of a page appears after a few visits: a small share of visitors contribute one, so a busy page costs almost nothing to keep current. Refresh picture takes a new one, and Pin picture keeps the current one whatever changes.

Findings is the ranked list of what is going wrong, ordered by how many people each thing affects rather than how often it happens: forty visitors clicking a broken button once each matters more than one visitor clicking it forty times. Every finding states the rule it applied and links to a replay of somebody hitting it.

The replay player

A recording plays back what the visitor saw. Idle and away time is skipped automatically, so a twenty-minute visit with two minutes of activity plays in two. The panel beside the player has four tabs.

Activity is what the visitor did, in order, lighting up as playback reaches it: clicks, typing, page changes, the keys that are decisions such as Enter and Escape, and the moments the page stopped responding. Overview is who they were, what they were on, and how fast the page was for them, banded against the same thresholds Google uses so a number here means what it means in Search Console. Console is what the page logged, with errors shown as errors rather than as empty braces. Network is everything the page asked for and everything it loaded, each row saying what asked for it and how long the visitor waited, and opening to show the timing, the headers, and the bodies for the addresses you have named.

Copy bug report puts the page, the user, the errors and a link to this recording on the clipboard. Share makes a read-only link that opens this one recording without a sign-in.

Sites and keys

Each site has its own key and its own snippet. The key is shown once, when it is issued; only a hash is kept, so there is no copy to show later. Replace key issues a new one and stops the old one immediately, so update the snippet in the same change. Rules for what to keep live under each site; they take effect on the next page load without a deploy.

Being told when something breaks

Spectra already notices when a page stops working. It watches how often the people on a page click the things people normally click there, and how often a given failure happens compared with how often it usually happens, and when either changes sharply it starts recording the visitors it affects. What it could not do was tell you, so all of that waited until somebody opened the dashboard.

Under each site, in Tell me when it breaks, paste a Slack incoming webhook or any address of your own that accepts a POST. You get one message when a problem starts, naming the page and how much worse it has got, with a link to the recordings. One message per problem, not one per check: something that repeats itself every few minutes is something people turn off, and an alert nobody reads is worse than no alert because it feels like coverage.

If you set a signing secret, every message carries a X-Spectra-Signature header: sha256= followed by an HMAC-SHA256 of the exact body, keyed with your secret. Recompute it over the raw bytes you received and compare. That is how your receiver knows the message came from us. The secret is never shown again after you save it, not even to you.

Team and invitations

Invite colleagues by email from the Team page. An invitation link is good for seven days and is also shown to you, so you can pass it on if the email does not arrive. Roles: an owner pays and can see billing; an admin manages people, sites and keys; a member watches recordings and edits rules; a viewer reads. Anyone with admin or above can change a role or remove a person, but not themselves. You only see what your role allows, so a page you have no use for is not shown to you.

You can be in two workspaces: one of your own and one you were invited to. When you are in both, a workspace picker appears at the bottom of the sidebar, and everything on the page follows the one you choose. To join a different workspace, leave one first from its Team page. The owner of a workspace cannot leave it, since the subscription belongs to them.

Plan and quota

Billing shows your plan, the sessions kept this period against your allowance, and your retention window. When the allowance is used up, new recordings pause until the period resets or you move up a plan; nothing already kept is affected. Upgrades apply immediately and are prorated by the payment provider. Invoices and payment details are managed from the same page.

What counts as friction

A recording is kept when something went wrong or somebody struggled. These are the reasons, in the words the session list and the Issues page use. Nothing here needs configuring; they are what the recorder watches for on every page.

ReasonWhat it means
Rage clickThe same control clicked over and over in a burst. Almost always means the control answered too slowly to feel like it had answered at all, rather than that it was broken.
Dead clickA click on something that looks interactive and produced no response: nothing navigated, nothing changed, no request started. Judged after a pause, and a click that started a request is never called dead just because the answer was slow.
Form errorA field was rejected. Reported once per attempt rather than once per keystroke, so clearing a box to retype it is not a finding.
Form thrashingThe same field edited and re-edited without the form moving on. The shape of somebody who cannot work out what a field wants.
Abandoned after effortReal work put in, and then the visitor left with nothing completed.
StruggleRepeated effort with no progress: submitting and returning to the same screen, hunting between the same controls.
Stuck loadingA loading indicator that never resolved. See the stuckLoadingMs option for the threshold.
Slow responseA request somebody was waiting on took far too long and then succeeded. A failure is already a finding; this is the one that is invisible otherwise, because nothing broke.
Abandoned a blank pageThe page never rendered anything and the visitor left.
Page did not renderThe page loaded but the things people interact with are not there, which is what a failed deeplink or a broken route looks like.
JS error, Unhandled rejection, Console errorAn error was thrown, a promise rejected with nobody handling it, or your code logged an error.
Failed request, Unreachable requestA request came back as a failure, or never completed at all: refused, unresolvable, or the network went away. The two are separated because only the second means the address could not be reached.
Resource failedA script or stylesheet the page needs did not load.
Blocked by policyYour own Content-Security-Policy refused something the page tried to load.
Install testA recording you asked for from the Sites page, to prove the snippet works.

You can add reasons of your own with track(), setProps() or a data-spectra-trigger attribute, and turn them into recordings with rules. See Recording rules.

What analytics collect

Analytics answers questions that only exist across many visits: which pages people reach, where they stop, where they click, how far they scroll, how fast pages were. It works differently from recordings and it is worth knowing how, because the honest answer to "are you tracking my visitors" depends on it.

A recording is kept only when something happened. Analytics needs every visit, so each page life sends one small anonymous summary as the page is left: which page, how long, whether the visitor interacted, how far they scrolled, which elements were clicked, the speed the browser measured, and where they came from. It is a summary, not a stream: there is no replay in it and no way to reconstruct one from it.

What it never contains: no name, no address, no identifier assigned by us that outlives the visit unless you have set one with identify(), and no text from the page. Element labels go through the same masking your recordings do, so a button whose text you have masked is a button whose label is masked here too.

It is collected only for sites whose workspace has the Analytics add-on. A site without it sends nothing at all, rather than sending it and having it discarded at our end. Set analytics: false to opt a site out even with the add-on active.

Heatmaps need one more thing: a picture of the page to draw on. A small fraction of visitors to a page that has no usable picture contribute one, which is the page as it was built in their browser, with the same masking applied. Everybody else contributes nothing to it.

On-Device Privacy and PII Masking

Spectra redacts sensitive information client-side prior to network dispatch. Passwords, credit card numbers, and auth tokens are stripped at the DOM serialization layer.

HTML Attribute Behavior
data-spectra-mask Replaces text contents and form field values with asterisks.
data-spectra-block Completely hides the element and all child nodes, replacing it with a placeholder box.
data-spectra-unmask Overrides global strict masking for public, non-sensitive content elements.

Erasing one person's recordings

When somebody asks you to delete their data, this removes every session recorded for them on a site: the sealed recordings, anything still being written, the search index entries, and the off-box copy if you have one. It needs the admin role in the workspace that owns the site.

Authenticate with your dashboard session token, not the ingest key. The ingest key can only write recordings; it cannot read or delete them, which is what keeps a key pasted into a public page from being able to erase your history. Open your browser's developer tools while signed in to the dashboard and copy the Authorization header from any request, or use the Sites page, which does the same thing for you.

Delete one visitor's recordings
curl -X DELETE "https://api.spectra-trace.com/v1/user-data?site=site_xxxxxxxx&user=user_94821" -H "Authorization: Bearer YOUR_DASHBOARD_TOKEN"

The reply says how many sessions were found and how many were removed. If a stored copy cannot be deleted the whole request fails rather than reporting a partial erasure as complete, so it is safe to retry.

Deleting a site

Deleting a site deletes everything recorded under it, everywhere it is kept. It cannot be undone and there is no recycle bin. Also admin or higher.

Delete a site and its recordings
curl -X DELETE "https://api.spectra-trace.com/v1/sites?site=site_xxxxxxxx" -H "Authorization: Bearer YOUR_DASHBOARD_TOKEN"