Skip to main content

Authentication

The Sealmetrics API supports two authentication methods. API keys (prefixed sm_, sent in the X-API-Key header) are read-only and intended for server-to-server integrations; JWT bearer tokens (obtained by logging in, sent in the Authorization: Bearer header) carry a user's role-based scopes and are used for authenticated sessions like the dashboard and mobile apps.

Session cookies are not measurement

Sealmetrics measures cookieless: the tracking script writes nothing to your visitors' browsers — no cookies, no local storage, no persistent identifier. The session identifier it sends is ephemeral and rotates daily.

The cookies described on this page do something else. They keep you signed in to the Sealmetrics dashboard and API, the way any web application does. They are set on my.sealmetrics.com, never on your site, they are never sent to a visitor's browser, and they carry no analytics data.

API Keys​

API keys are the recommended method for server-to-server integrations.

Obtaining an API Key​

  1. Log in to Sealmetrics dashboard
  2. Go to Settings → API Keys
  3. Click Generate New Key
  4. Copy the key immediately (it won't be shown again)

Using API Keys​

Include the key in the X-API-Key header:

curl -X GET "https://my.sealmetrics.com/api/v1/stats/overview?site_id=YOUR_SITE_ID&period=7d" \
-H "X-API-Key: sm_your_api_key_here"

API Key Format​

All API keys use the sm_ prefix:

sm_abc123def456ghi789...

Keys are 64 characters long and contain only alphanumeric characters after the prefix.

API Key Security​

  • Keys are hashed before storage (we cannot retrieve your key)
  • Keys can be revoked at any time from the dashboard
  • Set expiration dates for temporary access
  • Each key is scoped to specific sites
  • API keys are read-only: they can only hold the stats:read, sites:read, and accounts:read scopes. Write operations require a JWT session. See API Tokens for details.

Registration​

POST /auth/register​

Public endpoint for self-service signup. Creates a new user account and sends a verification email. No session is created until the email is verified — verifying it logs the user in. The user then creates an organization, which starts on the free tier (1,000,000 events total, not reset monthly); no plan selection or Stripe checkout is required to sign up.

curl -X POST "https://my.sealmetrics.com/api/v1/auth/register" \
-H "Content-Type: application/json" \
-d '{
"email": "alice@acme.com",
"name": "Alice",
"password": "a-strong-password-12+chars",
"accept_terms": true
}'

Request Body:

FieldTypeRequiredDescription
emailstringYesValid email
namestringYes1-255 chars
passwordstringYes12-128 chars; must satisfy the platform password policy
accept_termsbooleanYesMust be true. Otherwise 400

Response (200 OK):

{
"success": true,
"data": {
"user_id": 42,
"email": "alice@acme.com",
"name": "Alice",
"access_token": "",
"token_type": "bearer",
"expires_in": 0,
"requires_email_verification": true,
"requires_subscription": false,
"session_created": false,
"email_sent": true,
"message": "Check your email to continue registration"
}
}

Status code: Always 200 OK (not 201) — the user is created but not yet fully provisioned (email pending).

Notable behavior:

  • Rate limited: 3 registrations per hour per IP.
  • Enumeration-safe: if the email already exists, the response is identical to a successful signup (user_id: 0, generic message). A verified account receives a one-time security notification; an unverified one has its verification email re-sent to the address. The existing account's credentials are never changed.
  • No session at signup: access_token stays empty and no cookies are set. The one exception is a signup that started from a pending MCP OAuth consent: then session_created is true and the response sets access_token and refresh_token cookies (scopes: read, write) so the consent can resume.
  • Creating an organization requires a verified email and is refused if the user already belongs to an organization.
  • Email verification: verifies via the POST /email/verify endpoint, which then performs the auto-login.

JWT Bearer Tokens​

JWT tokens are used for user-authenticated sessions (dashboard, mobile apps).

Obtaining a Token​

curl -X POST "https://my.sealmetrics.com/api/v1/auth/token" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "your_password"
}'

Response:

{
"success": true,
"data": {
"access_token": "<access_token>",
"token_type": "bearer",
"expires_in": 900,
"user": {
"id": 123,
"email": "user@example.com",
"name": "John Doe",
"role": "admin",
"account_ids": ["acme-corp"]
},
"ip_filtered_accounts": [],
"client_ip": "203.0.113.45"
}
}

The refresh token is not returned in the response body. It is set as an HttpOnly cookie named sm_refresh_token (the access token is also mirrored in the sm_access_token cookie).

Using JWT Tokens​

Include the token in the Authorization header:

curl -X GET "https://my.sealmetrics.com/api/v1/stats/overview?site_id=YOUR_SITE_ID&period=7d" \
-H "Authorization: Bearer <your_access_token>"

Token Refresh​

Access tokens expire after 15 minutes. Use the refresh endpoint to obtain a new access token. The refresh token is read from the sm_refresh_token HttpOnly cookie that was set during login — there is no request body. Send the cookie back with the request (--cookie in curl, credentials: 'include' in fetch):

curl -X POST "https://my.sealmetrics.com/api/v1/auth/refresh" \
--cookie "sm_refresh_token=<your_refresh_token>"

On success, a new access token is returned in the response body (wrapped in data) and the rotated refresh token is set again as the sm_refresh_token cookie (session rotation).

JWT Claims​

The access token contains:

ClaimDescription
subUser ID
emailUser email
account_idsList of accessible site IDs
scopesScopes derived from the user's role
expExpiration timestamp
iatIssued at timestamp

JWT Scopes​

Scopes are assigned automatically based on the user's role — you do not request them in the /auth/token body and you cannot add them at runtime. To change what a token can do, change the user's role.

RoleScopes
Standard userread, write
Superadminread, write, admin, superadmin, billing:manage

Organization owners additionally receive the billing:manage scope so they can reach billing endpoints. Most authorization is enforced through organization roles (owner / admin / member) rather than JWT scopes.

If a request returns 403 forbidden with a valid JWT, the user's role doesn't include the required scope — switch to a user with the right role, or use an API key bound to the site.

Password reset​

Two endpoints handle the flow: POST /auth/forgot-password (request the email; always returns success to prevent enumeration; rate limited 3/hour/IP) and POST /auth/reset-password (complete the reset with the emailed token; terminates all existing sessions).

Full request/response details: Advanced Authentication — Password Reset.

Email verification helpers​

POST /email/resend-verification​

Resend the verification email if the user hasn't received (or has lost) the original one. Rate limited to 3/hour/IP and returns a single generic response for every branch (missing email / already verified / sent / cap exceeded) — deliberate anti-enumeration.

curl -X POST "https://my.sealmetrics.com/api/v1/email/resend-verification" \
-H "Content-Type: application/json" \
-d '{"email": "alice@acme.com"}'

Session management​

Once logged in, users can inspect and revoke their own sessions (GET /auth/sessions, DELETE /auth/sessions/{session_id}, DELETE /auth/sessions for all devices, POST /auth/logout). All require a JWT — API keys don't manage sessions.

Full request/response details: Advanced Authentication — Session Management.

Two-Factor Authentication (2FA)​

If a user has 2FA enabled, POST /auth/token returns a two-step response: the first call succeeds only up to the point of requiring the second factor, and the client must exchange the returned challenge token via the 2FA endpoints to obtain a full session.

See the dedicated Two-Factor Authentication page for the full endpoint list (/2fa/setup, /2fa/verify, /2fa/verify-login, /2fa/disable, /2fa/backup-codes, /2fa/status).

Impersonation status​

GET /auth/impersonation-status​

Returns whether the current session is a superadmin impersonating another user. Useful for the dashboard to render an "You are impersonating X" banner and gate destructive actions.

curl -X GET "https://my.sealmetrics.com/api/v1/auth/impersonation-status" \
--cookie "sm_access_token=<jwt>"

Response (regular session):

{ "success": true, "data": { "is_impersonating": false } }

Response (superadmin impersonating):

{
"success": true,
"data": {
"is_impersonating": true,
"impersonated_user_id": 123,
"impersonated_email": "target@customer.com",
"started_at": "2026-07-20T09:00:00Z"
}
}

Authentication Errors​

The error.code is derived from the HTTP status (401 → unauthorized, 403 → forbidden, 429 → rate_limit_exceeded). The specific reason is carried in the human-readable error.message.

HTTP CodeError CodeExample message
401unauthorizedAuthentication required (no API key or token provided)
401unauthorizedInvalid API key (key not found or revoked)
401unauthorizedInvalid token (JWT is malformed)
401unauthorizedToken has expired
401unauthorizedSession has been revoked
403forbiddenRequired scope: <scope> (valid auth, missing scope)
403forbiddenAccess denied to account: <id>

Example error response:

{
"error": {
"code": "unauthorized",
"message": "Invalid API key"
},
"request_id": "req_abc123"
}

Best Practices​

For API Keys​

  1. Never commit keys to git - Use environment variables
  2. Rotate keys periodically - Generate new keys and revoke old ones
  3. Use separate keys per environment - Different keys for dev, staging, production
  4. Set minimum required scope - Only grant access to needed sites

For JWT Tokens​

  1. Store tokens securely - Use httpOnly cookies or secure storage
  2. Implement token refresh - Don't wait for expiration errors
  3. Handle 401 gracefully - Redirect to login or refresh automatically
  4. Clear tokens on logout - Call the logout endpoint

Code Examples​

Python​

import requests

API_KEY = "sm_your_api_key_here"
BASE_URL = "https://my.sealmetrics.com/api/v1"

def get_stats(site_id: str, period: str = "7d"):
response = requests.get(
f"{BASE_URL}/stats/overview",
headers={"X-API-Key": API_KEY},
params={"site_id": site_id, "period": period}
)
response.raise_for_status()
return response.json()

JavaScript / Node.js​

const API_KEY = 'sm_your_api_key_here';
const BASE_URL = 'https://my.sealmetrics.com/api/v1';

async function getStats(siteId, period = '7d') {
const response = await fetch(
`${BASE_URL}/stats/overview?site_id=${siteId}&period=${period}`,
{
headers: { 'X-API-Key': API_KEY }
}
);

if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}

return response.json();
}

PHP​

<?php
$apiKey = 'sm_your_api_key_here';
$baseUrl = 'https://my.sealmetrics.com/api/v1';

function getStats($siteId, $period = '7d') {
global $apiKey, $baseUrl;

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "$baseUrl/stats/overview?site_id=$siteId&period=$period");
curl_setopt($ch, CURLOPT_HTTPHEADER, ["X-API-Key: $apiKey"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

return json_decode($response, true);
}

Other ways to authenticate​

MethodPageUse it when
OAuth 2.1 + PKCEOAuth 2.1Your app or AI assistant needs to read someone else's account. Dynamic client registration, refresh tokens, user-revocable from the dashboard.
Provision keyProvisioningYou are creating a brand-new free-tier account headlessly, before any credentials exist.
Written and maintained by the Sealmetrics Team