Authentication
API key management — quick-start legacy key or scoped database-backed keys for production
Every request to the admin API requires a bearer token in the Authorization header. Hogsend supports two authentication modes — a legacy environment-variable key for quick setup, and database-backed keys with scoped permissions for production use.
How Auth Works
When a request hits any /v1/admin/* endpoint, the auth middleware runs this sequence:
- Extract the
Authorization: Bearer <token>header - Check if the token matches the
ADMIN_API_KEYenvironment variable (legacy key, full access) - If no match, SHA-256 hash the token and look it up in the
apiKeysdatabase table - Verify the key is not revoked and not expired
- Check that the key's scopes permit the requested operation (GET requires
read, mutations requirejourney-adminorfull-admin) - If neither mode matches, return
401 Unauthorized
If no admin key is configured at all (no ADMIN_API_KEY env var, no database keys), admin endpoints return 503 Service Unavailable.
Active database-backed keys are cached in memory for 60 seconds. A newly created key works immediately (cache miss triggers a DB lookup). A revoked key may still work for up to 60 seconds until its cache entry expires.
Legacy Key (Quick Start)
Set the ADMIN_API_KEY environment variable and you are ready to go:
# .env
ADMIN_API_KEY=hsk_your-secret-admin-key-at-least-32-charscurl -H "Authorization: Bearer hsk_your-secret-admin-key-at-least-32-chars" \
http://localhost:3002/v1/admin/contactsThe legacy key always has full admin access -- it can perform any operation. This is fine for local development and single-operator setups, but for production you should create scoped keys.
Creating Your First Database-Backed Key
Use the legacy key (or any existing full-admin key) to create a new scoped key:
curl -X POST http://localhost:3002/v1/admin/api-keys \
-H "Authorization: Bearer $ADMIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Ops Dashboard",
"scopes": ["read"],
"expiresAt": "2026-12-31T00:00:00.000Z"
}'{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Ops Dashboard",
"key": "hsk_abc123def456ghi789jkl012mno345pqr678",
"keyPrefix": "hsk_abc1",
"scopes": ["read"],
"expiresAt": "2026-12-31T00:00:00.000Z",
"createdAt": "2026-01-15T10:30:00.000Z"
}Save the key value immediately. It is SHA-256 hashed before storage and will never be shown again. If you lose it, revoke the key and create a new one.
The keyPrefix field (first 8 characters) is stored in plaintext so you can identify keys in list views without exposing the full token.
Key Scopes
Each database-backed key has one or more scopes that control what it can do:
| Scope | What it allows |
|---|---|
read | All GET endpoints -- list contacts, view metrics, browse events, inspect journeys |
journey-admin | Everything in read, plus journey management -- enable/disable journeys, enroll users, cancel instances |
full-admin | Everything -- key management, bulk imports/exports, alert configuration, event replay, email resend |
Scopes are cumulative -- a key with ["journey-admin"] implicitly has read access. A key with ["full-admin"] can do everything.
Which scope do I need?
| Task | Required scope |
|---|---|
| View metrics dashboard | read |
| List contacts | read |
| Browse email history | read |
| View journey states | read |
| Enable/disable a journey | journey-admin |
| Enroll a user in a journey | journey-admin |
| Cancel a journey instance | journey-admin |
| Create or revoke API keys | full-admin |
| Import contacts | full-admin |
| Export contacts | full-admin |
| Create alert rules | full-admin |
| Replay events | full-admin |
| Resend failed emails | full-admin |
| Retry DLQ entries | full-admin |
A request with insufficient scope returns 403 Forbidden:
{ "error": "Insufficient scope" }Key Lifecycle
Create
curl -X POST http://localhost:3002/v1/admin/api-keys \
-H "Authorization: Bearer your-api-key" \
-H "Content-Type: application/json" \
-d '{
"name": "CI Pipeline",
"scopes": ["read"],
"expiresAt": "2026-06-01T00:00:00.000Z"
}'The name field is for your reference -- use it to identify what the key is for when reviewing the key list or audit logs. The expiresAt field is optional; keys without an expiry date are valid until revoked.
Use
Include the key as a bearer token in the Authorization header:
curl -H "Authorization: Bearer hsk_abc123def456ghi789..." \
http://localhost:3002/v1/admin/contactsThe lastUsedAt timestamp on the key record updates when the key is used, so you can identify stale keys that should be cleaned up.
List
curl -H "Authorization: Bearer your-api-key" \
http://localhost:3002/v1/admin/api-keysRevoked keys are hidden by default. To see them:
curl -H "Authorization: Bearer your-api-key" \
"http://localhost:3002/v1/admin/api-keys?includeRevoked=true"Rotate
There is no in-place rotation endpoint. To rotate a key:
- Create a new key with the same name and scopes
- Update your systems to use the new key
- Revoke the old key
# Step 1: Create replacement
curl -X POST http://localhost:3002/v1/admin/api-keys \
-H "Authorization: Bearer your-api-key" \
-H "Content-Type: application/json" \
-d '{ "name": "CI Pipeline (rotated)", "scopes": ["read"] }'
# Step 2: Update your CI config with the new key...
# Step 3: Revoke the old key
curl -X DELETE http://localhost:3002/v1/admin/api-keys/old-key-uuid \
-H "Authorization: Bearer your-api-key"Revoke
curl -X DELETE http://localhost:3002/v1/admin/api-keys/key-uuid \
-H "Authorization: Bearer your-api-key"Revocation sets revokedAt on the key record and immediately invalidates the in-memory cache entry. Due to the 60-second cache TTL, the revoked key may still be accepted by other API instances for a brief window in multi-instance deployments.
Best Practices
Use read-only keys for dashboards. Any monitoring tool, dashboard, or reporting pipeline that only reads data should use a read-scoped key. This limits the blast radius if the key is leaked.
Use journey-admin keys for automation tools. If you have a tool that enables/disables journeys or enrolls users, give it journey-admin scope. It does not need to manage API keys or run imports.
Reserve full-admin for operators. Only human operators and trusted automation (like your deploy pipeline) should have full-admin keys.
Set expiry dates on CI/CD keys. Pipeline keys should expire and be rotated regularly. Set a 90-day expiry and automate key rotation in your CI config.
Use short-lived keys for one-off tasks. Running a data migration? Create a full-admin key with a 24-hour expiry. It auto-expires when you are done.
Name keys descriptively. The name appears in audit logs. "CI Pipeline" or "Grafana Dashboard" is much more useful than "key1" when you are investigating an incident.
Audit key usage periodically. List your keys and check lastUsedAt -- revoke any key that has not been used in months.
Rate Limiting
All API endpoints (admin and non-admin) are protected by a sliding-window rate limiter:
| Setting | Value |
|---|---|
| Window | 1 minute |
| Max requests | 100 per API key |
| Backend | Redis (primary), in-memory fallback |
Rate limit status is communicated via response headers:
| Header | Description |
|---|---|
X-RateLimit-Remaining | Requests remaining in the current window |
Retry-After | Seconds until the next request is accepted (only on 429) |
When the limit is exceeded:
HTTP/1.1 429 Too Many Requests
Retry-After: 32{ "error": "Rate limit exceeded" }To stay under the limit: use bulk endpoints (/import, /enroll/batch, /replay) instead of looping individual calls, and cache read responses client-side when possible.
Troubleshooting
401 Unauthorized
The token is missing, malformed, or does not match any known key.
- Verify the
Authorizationheader format:Bearer <token>(note the space) - Check that the key has not been revoked (
GET /v1/admin/api-keys?includeRevoked=true) - Check that the key has not expired
- If using the legacy key, verify
ADMIN_API_KEYis set in your environment
403 Forbidden
The key is valid but does not have the required scope for this operation.
- Check the key's scopes (
GET /v1/admin/api-keys) - POST/PATCH/DELETE operations require
journey-adminorfull-adminscope - Key management and bulk operations require
full-adminscope
429 Too Many Requests
You have exceeded the rate limit (100 requests per minute per key).
- Read the
Retry-Afterheader to know when to retry - Use bulk endpoints instead of individual calls
- If you need higher limits, adjust the rate limit configuration
503 Service Unavailable
No authentication method is configured. The API cannot accept admin requests.
- Set the
ADMIN_API_KEYenvironment variable, or - Create at least one database-backed key (requires the legacy key to bootstrap)
For the full endpoint specification of key management endpoints, see the API Reference.
Production Security Checklist
When deploying Hogsend to production:
- Set a strong
ADMIN_API_KEY— at least 32 random characters - Set a strong
BETTER_AUTH_SECRET— used for session signing - Use HTTPS — all API keys are transmitted in headers; never use HTTP in production
- Create scoped keys — avoid sharing the legacy key; create read-only keys for monitoring
- Set key expiry dates — especially for CI/CD and one-time migration keys
- Monitor audit logs — periodically review for unexpected mutations
- Configure Resend webhook verification — set
RESEND_WEBHOOK_SECRETto validate webhook payloads - Review rate limit settings — 100 req/min is suitable for most teams but can be adjusted via environment configuration