Hardened ingest (signed tokens)
Verify short-lived signed tokens your server mints so forged reports can't be trusted. Includes sample code.
Your project API key is public — it ships in the browser SDK, so anyone can read it and send forged error reports. Hardened ingest removes that trust problem: your server mints a short-lived signed token that the browser attaches to each report, and error.page verifies the signature with a secret the browser never sees. A forger without your secret can't produce a token we'll trust.
Available on every plan. Enable it per project under Projects → Security & settings.
Open standards
Hardened ingest is built entirely on open standards, so you can mint tokens in any language with an off-the-shelf library:
- JSON Web Token — RFC 7519 (token format + registered claims)
- JWS / HS256 — RFC 7515 + RFC 7518 (HMAC-SHA256 signature)
- Bearer token — RFC 6750 (
Authorization: Bearer …transport)
How it works
-
Enable Hardened ingest on the project and copy the signing secret (shown once). Store it as a server-side secret — never ship it to the browser.
-
Your server mints a JWT signed with that secret, using these claims:
Claim Value audyour project's API key (binds the token to this project) iatissued-at (Unix seconds) expexpiry — no further out than your configured token max lifetime sub(optional) a stable per-user/session id — used for the accurate "users affected" count -
Hand the token to your frontend and pass it to the SDK; it's sent as
Authorization: Bearer <token>. -
error.page verifies the signature, audience and expiry, and marks the incident signed (trusted).
You control duration and interval
- Token max lifetime (duration) — the longest token you'll accept. We reject any token whose
exp − iatexceeds it, so a leaked token can't be long-lived. Keep it short (minutes). - Refresh interval — how often your frontend should fetch a fresh token. Set it below the max lifetime; the SDK re-mints on this cadence.
- Strict mode — reject anything that isn't signed (or authenticated with
X-Secret-Key). Without strict mode, unsigned reports are still accepted but flagged unverified and excluded from your dashboard metrics.
Rotating the secret
Rotate the signing secret anytime from the project settings. Rotation is immediate: tokens signed with the old secret stop verifying, so roll it out to your server in step.
Sample code — minting a token
Each snippet mints a token valid for 120 seconds, bound to your project. Serve the result from a small authenticated endpoint your frontend can call.
Node.js
const jwt = require('jsonwebtoken'); // npm i jsonwebtoken
const now = Math.floor(Date.now() / 1000);
const token = jwt.sign(
{ aud: process.env.ERRORPAGE_API_KEY, iat: now, exp: now + 120, sub: userId },
process.env.ERRORPAGE_SIGNING_SECRET,
{ algorithm: 'HS256' }
);
Python
import time, jwt # pip install PyJWT
now = int(time.time())
token = jwt.encode(
{"aud": ERRORPAGE_API_KEY, "iat": now, "exp": now + 120, "sub": user_id},
ERRORPAGE_SIGNING_SECRET,
algorithm="HS256",
)
PHP
use Firebase\JWT\JWT; // composer require firebase/php-jwt
$now = time();
$token = JWT::encode(
['aud' => $errorpageApiKey, 'iat' => $now, 'exp' => $now + 120, 'sub' => $userId],
$errorpageSigningSecret,
'HS256'
);
Ruby
require 'jwt' # gem install jwt
now = Time.now.to_i
token = JWT.encode(
{ aud: ERRORPAGE_API_KEY, iat: now, exp: now + 120, sub: user_id },
ERRORPAGE_SIGNING_SECRET,
'HS256'
)
Go
import (
"time"
"github.com/golang-jwt/jwt/v5" // go get github.com/golang-jwt/jwt/v5
)
now := time.Now().Unix()
token, _ := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"aud": apiKey, "iat": now, "exp": now + 120, "sub": userID,
}).SignedString([]byte(signingSecret))
Java
// com.auth0:java-jwt
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import java.time.Instant;
Instant now = Instant.now();
String token = JWT.create()
.withAudience(apiKey)
.withIssuedAt(now)
.withExpiresAt(now.plusSeconds(120))
.withSubject(userId)
.sign(Algorithm.HMAC256(signingSecret));
Wiring the browser SDK
Give the SDK a getToken callback that returns a current token (it may return a Promise, so you can fetch one from your endpoint). The SDK attaches it as a Bearer token on every report and re-requests on your refresh interval:
<script src="https://error.page/js/errorpage.js"></script>
<script>
ErrorPage.init({
projectId: 'YOUR_API_KEY',
slug: 'your-project-slug',
getToken: function () {
// Return a token (or a Promise for one) minted by YOUR server.
return fetch('/errorpage-token').then(function (r) { return r.text(); });
}
});
</script>
Verifying it works
With hardened ingest on (non-strict), send a report with and without a token and watch the incident feed: signed reports show a 🔒 signed badge, unsigned ones show ⚠ unverified and are kept out of your headline metrics. Turn on strict mode once your integration is minting tokens reliably.