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.
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://api.sealmetrics.com/api/v1/sites/YOUR_SITE_ID/stats" \
-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, sends a verification email, and (when billing is enabled) creates a temporary session so the user can proceed to plan selection / Stripe checkout without having verified their email first.
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": "eyJhbGciOi...",
"token_type": "bearer",
"expires_in": 3600,
"requires_email_verification": true,
"requires_subscription": true,
"email_sent": true,
"message": "Registration successful. Choose your plan to continue."
}
}
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), and a one-time security notification is sent to the existing user. - Auth cookies: when billing is enabled, the response sets
access_tokenandrefresh_tokencookies and includes the access token in the body (scopes:read,write,billing:checkout). When billing is disabled, no session is created and the token fields stay empty. - 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://api.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://api.sealmetrics.com/api/v1/sites/YOUR_SITE_ID/stats" \
-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://api.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.
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://api.sealmetrics.com/api/v1"
def get_stats(site_id: str, period: str = "7d"):
response = requests.get(
f"{BASE_URL}/sites/{site_id}/stats",
headers={"X-API-Key": API_KEY},
params={"period": period}
)
response.raise_for_status()
return response.json()
JavaScript / Node.js
const API_KEY = 'sm_your_api_key_here';
const BASE_URL = 'https://api.sealmetrics.com/api/v1';
async function getStats(siteId, period = '7d') {
const response = await fetch(
`${BASE_URL}/sites/${siteId}/stats?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://api.sealmetrics.com/api/v1';
function getStats($siteId, $period = '7d') {
global $apiKey, $baseUrl;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "$baseUrl/sites/$siteId/stats?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);
}