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.

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 SettingsAPI 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://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, 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, 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:

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": "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_token and refresh_token cookies 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/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://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:

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.

Authentication Errors

The error.code is derived from the HTTP status (401unauthorized, 403forbidden, 429rate_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://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);
}