Hogsend
Operating

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:

  1. Extract the Authorization: Bearer <token> header
  2. Check if the token matches the ADMIN_API_KEY environment variable (legacy key, full access)
  3. If no match, SHA-256 hash the token and look it up in the apiKeys database table
  4. Verify the key is not revoked and not expired
  5. Check that the key's scopes permit the requested operation (GET requires read, mutations require journey-admin or full-admin)
  6. 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-chars
curl -H "Authorization: Bearer hsk_your-secret-admin-key-at-least-32-chars" \
  http://localhost:3002/v1/admin/contacts

The 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:

ScopeWhat it allows
readAll GET endpoints -- list contacts, view metrics, browse events, inspect journeys
journey-adminEverything in read, plus journey management -- enable/disable journeys, enroll users, cancel instances
full-adminEverything -- 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?

TaskRequired scope
View metrics dashboardread
List contactsread
Browse email historyread
View journey statesread
Enable/disable a journeyjourney-admin
Enroll a user in a journeyjourney-admin
Cancel a journey instancejourney-admin
Create or revoke API keysfull-admin
Import contactsfull-admin
Export contactsfull-admin
Create alert rulesfull-admin
Replay eventsfull-admin
Resend failed emailsfull-admin
Retry DLQ entriesfull-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/contacts

The 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-keys

Revoked 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:

  1. Create a new key with the same name and scopes
  2. Update your systems to use the new key
  3. 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:

SettingValue
Window1 minute
Max requests100 per API key
BackendRedis (primary), in-memory fallback

Rate limit status is communicated via response headers:

HeaderDescription
X-RateLimit-RemainingRequests remaining in the current window
Retry-AfterSeconds 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 Authorization header 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_KEY is 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-admin or full-admin scope
  • Key management and bulk operations require full-admin scope

429 Too Many Requests

You have exceeded the rate limit (100 requests per minute per key).

  • Read the Retry-After header 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_KEY environment 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:

  1. Set a strong ADMIN_API_KEY — at least 32 random characters
  2. Set a strong BETTER_AUTH_SECRET — used for session signing
  3. Use HTTPS — all API keys are transmitted in headers; never use HTTP in production
  4. Create scoped keys — avoid sharing the legacy key; create read-only keys for monitoring
  5. Set key expiry dates — especially for CI/CD and one-time migration keys
  6. Monitor audit logs — periodically review for unexpected mutations
  7. Configure Resend webhook verification — set RESEND_WEBHOOK_SECRET to validate webhook payloads
  8. Review rate limit settings — 100 req/min is suitable for most teams but can be adjusted via environment configuration

On this page