Authentication#
API Keys#
HashWatch uses API keys for authentication on all private endpoints. An API key looks like this:
<team-id>.<secret>For example: acme-ir.sk_live_a1b2c3d4e5f6...
Sending your API key#
Include your key in the X-API-Key header on every request:
curl https://api.hashwatch.us/api/v1/intel/stats \
-H "X-API-Key: acme-ir.sk_live_a1b2c3d4e5f6..."API keys are never logged or stored in plaintext — only a secure HMAC hash is kept on the server. Keep your key confidential; it cannot be recovered if lost (request a new one from your administrator).
JWT Tokens (optional)#
For high-throughput use cases, you can exchange your API key for a short-lived JWT token (15-minute TTL) and use that for subsequent requests. This avoids the HMAC verification overhead on every call.
Obtain a token#
curl -X POST https://api.hashwatch.us/api/v1/auth/token \
-H "X-API-Key: acme-ir.sk_live_a1b2c3d4e5f6..." \
-H "Content-Type: application/json"Response:
{
"token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"expires_at": "2026-06-05T03:00:00Z"
}Use the token#
curl https://api.hashwatch.us/api/v1/intel/stats \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."Refresh before expiry#
curl -X POST https://api.hashwatch.us/api/v1/auth/refresh \
-H "Authorization: Bearer <current-token>"The public key used to verify JWT signatures is available at:
GET https://api.hashwatch.us/.well-known/jwks.jsonRoles and Tiers#
Every API key has an assigned role (what actions are allowed) and tier (billing gate for paid features). Both must be satisfied for a request to succeed.
Permissions#
| Permission | Endpoint | Minimum Tier |
|---|---|---|
intel:stats | GET /api/v1/intel/stats | free |
intel:history | GET /api/v1/intel/history | paid |
intel:downloads | GET /api/v1/intel/downloads | paid |
intel:revocations | GET /api/v1/intel/revocations | paid |
keys:manage | /api/v1/admin/keys/* | enterprise |
Roles#
| Role | intel:stats | intel:history | intel:downloads | intel:revocations | keys:manage |
|---|---|---|---|---|---|
analyst | ✓ | ||||
responder | ✓ | ||||
auditor | ✓ | ✓ | ✓ | ✓ | |
responder_full | ✓ | ✓ | ✓ | ✓ | |
admin | ✓ | ✓ | ✓ | ✓ | ✓ |
Tiers#
| Tier | What it unlocks |
|---|---|
free | intel:stats |
paid | intel:history, intel:downloads (when role also grants them) |
enterprise | keys:manage (when role is admin) |
Error responses#
All errors return JSON with an error field:
{"error": "unauthorized"}
{"error": "forbidden"}
{"error": "rate limit exceeded"}| HTTP Status | Meaning |
|---|---|
401 Unauthorized | Missing or invalid API key / JWT |
403 Forbidden | Valid credential but insufficient role or tier |
429 Too Many Requests | Rate limit exceeded — back off and retry |
Rate limits#
| Endpoint group | Limit |
|---|---|
GET /public/hash-of-day | 100 requests / minute / IP |
POST /api/v1/auth/token | 10 requests / minute / key |
| All other authenticated endpoints | 60 requests / minute / key |
Rate limits are enforced at the Cloudflare edge before requests reach the application. Exceeding the limit returns HTTP 429. The Retry-After header (seconds) indicates when you may retry.
Admin authentication endpoints#
These endpoints are used by the Admin Panel web UI and by automation that manages admin sessions. They are separate from the team API key flow above — they authenticate platform administrators, not data consumers.
Token endpoints (current)#
| Method | Path | Description |
|---|---|---|
POST | /api/v1/auth/token | Exchange an API key for a short-lived JWT (15 min) |
POST | /api/v1/auth/refresh | Renew a JWT before expiry (returns a new 15-min token) |
GET | /.well-known/jwks.json | RSA public key for verifying JWT signatures |
See JWT Tokens above for request/response details.
Passkey (WebAuthn) endpoints#
Passkeys use the Web Authentication API (WebAuthn). Registration and authentication each require two calls: one to get a challenge from the server, and one to return the signed response from the browser/device.
Register a new passkey#
POST /api/v1/admin/auth/passkey/register/beginAuth required: valid admin JWT or API key
Request body: {"display_name": "MacBook Touch ID"}
Response: WebAuthn PublicKeyCredentialCreationOptions challenge object (pass to navigator.credentials.create())
POST /api/v1/admin/auth/passkey/register/completeAuth required: same session as begin
Request body: the PublicKeyCredential object returned by the browser
Response: {"passkey_id": "...", "display_name": "MacBook Touch ID", "created_at": "..."}
Sign in with a passkey#
POST /api/v1/admin/auth/passkey/login/beginAuth required: none
Request body: {} (or {"user_handle": "..."} to hint a specific account)
Response: WebAuthn PublicKeyCredentialRequestOptions challenge (pass to navigator.credentials.get())
POST /api/v1/admin/auth/passkey/login/completeAuth required: none
Request body: the PublicKeyCredential assertion returned by the browser
Response: {"token": "eyJ...", "expires_at": "..."} — same JWT format as POST /api/v1/auth/token
List and remove passkeys#
GET /api/v1/admin/auth/passkeys # list registered passkeys for the caller
DELETE /api/v1/admin/auth/passkeys/{id} # remove a passkey by IDBoth require a valid admin JWT.
TOTP endpoints#
Enroll TOTP#
POST /api/v1/admin/auth/totp/enrollAuth required: valid admin JWT
Request body: {}
Response:
{
"secret": "JBSWY3DPEHPK3PXP",
"otpauth_url": "otpauth://totp/HashWatch%3Aplatform-admin?secret=JBSWY3D...&issuer=HashWatch",
"qr_code_svg": "<svg>...</svg>"
}The otpauth_url can be scanned as a QR code or entered manually into any authenticator app.
Confirm TOTP enrollment#
After scanning the QR code, verify the first code to activate TOTP:
POST /api/v1/admin/auth/totp/verifyAuth required: valid admin JWT
Request body: {"code": "123456"}
Response: {"enrolled": true} — TOTP is now active for this account
Disable TOTP#
DELETE /api/v1/admin/auth/totpAuth required: valid admin JWT
Request body: {"code": "123456"} — must provide a valid current code to confirm
Response: {"disabled": true}
Recovery code endpoints#
Generate recovery codes#
POST /api/v1/admin/auth/recovery/generateAuth required: valid admin JWT
Request body: {}
Response:
{
"codes": [
"a3b7-f2d9-1e4c",
"9x2k-0m5p-7h3q",
"..."
],
"count": 10,
"warning": "Store these offline. Each code can only be used once."
}Calling this endpoint invalidates any previously generated codes for the account.
Sign in with a recovery code#
POST /api/v1/admin/auth/recovery/loginAuth required: none
Request body: {"code": "a3b7-f2d9-1e4c"}
Response: {"token": "eyJ...", "expires_at": "..."} — the used code is invalidated immediately
Remaining code count is returned in the
X-Recovery-Codes-Remainingresponse header so the admin panel can warn when codes are running low.