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.
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
- Log in to Sealmetrics dashboard
- Go to Settings → API Keys
- Click Generate New Key
- 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, andaccounts:readscopes. 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:
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Valid email |
name | string | Yes | 1-255 chars |
password | string | Yes | 12-128 chars; must satisfy the platform password policy |
accept_terms | boolean | Yes | Must 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_tokenstays empty and no cookies are set. The one exception is a signup that started from a pending MCP OAuth consent: thensession_createdistrueand the response setsaccess_tokenandrefresh_tokencookies (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/verifyendpoint, 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:
| Claim | Description |
|---|---|
sub | User ID |
email | User email |
account_ids | List of accessible site IDs |
scopes | Scopes derived from the user's role |
exp | Expiration timestamp |
iat | Issued 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.
| Role | Scopes |
|---|---|
| Standard user | read, write |
| Superadmin | read, 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 Code | Error Code | Example message |
|---|---|---|
| 401 | unauthorized | Authentication required (no API key or token provided) |
| 401 | unauthorized | Invalid API key (key not found or revoked) |
| 401 | unauthorized | Invalid token (JWT is malformed) |
| 401 | unauthorized | Token has expired |
| 401 | unauthorized | Session has been revoked |
| 403 | forbidden | Required scope: <scope> (valid auth, missing scope) |
| 403 | forbidden | Access denied to account: <id> |
Example error response:
{
"error": {
"code": "unauthorized",
"message": "Invalid API key"
},
"request_id": "req_abc123"
}
Best Practices
For API Keys
- Never commit keys to git - Use environment variables
- Rotate keys periodically - Generate new keys and revoke old ones
- Use separate keys per environment - Different keys for dev, staging, production
- Set minimum required scope - Only grant access to needed sites
For JWT Tokens
- Store tokens securely - Use httpOnly cookies or secure storage
- Implement token refresh - Don't wait for expiration errors
- Handle 401 gracefully - Redirect to login or refresh automatically
- 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
| Method | Page | Use it when |
|---|---|---|
| OAuth 2.1 + PKCE | OAuth 2.1 | Your app or AI assistant needs to read someone else's account. Dynamic client registration, refresh tokens, user-revocable from the dashboard. |
| Provision key | Provisioning | You are creating a brand-new free-tier account headlessly, before any credentials exist. |
Related
- For AI Agents — which method to pick, and the rest of the agent surface
- API Tokens — scopes, site restrictions and expiry
- Error codes — every
unauthorized/forbiddenvariant - Advanced Authentication — sessions, 2FA, password reset