Authentication

Posthawk uses API keys to authenticate requests. Each key has an environment (test or live) and a permission scope. Authenticate with Authorization: Bearer (recommended) or X-API-Key — both work.

All API endpoints that send or manage emails require authentication via an API key.

Create API keys from the dashboard under Settings → API Keys. Each key has an environment ("test" or "live") and a permission scope.

Include the key in every request. Posthawk accepts two authentication headers — use whichever fits your stack:

Recommended — `Authorization: Bearer` (the standard pattern used by Stripe, Resend, SendGrid, OpenAI, GitHub):

Authorization: Bearer your_api_key_here

Legacy / alternative — `X-API-Key` (the original Posthawk header, kept for backwards compatibility):

X-API-Key: your_api_key_here

Migrating from Resend, SendGrid, or Postmark? Your existing Authorization: Bearer ... code works as-is — no header changes needed. The TypeScript and Python SDKs handle this for you automatically.

API keys are hashed before storage — the full key is only shown once at creation time. Keep it secure.

If a request fails — bad key, missing scope, or anything else — the API returns a structured error envelope with a stable name and a requestId you can quote to support. See Errors for the full taxonomy and a copy-pasteable retry handler.

Environments: test vs live

Each key is created with one of two environments:

  • test — Calls succeed end-to-end (validation, template rendering, suppression checks, email_logs entry) but never hand off to AWS SES. The response includes a synthetic message_id of the form sandbox-{ts}-{rand}. Quota usage is NOT counted against your plan limits. Visible in the dashboard with a "Sandbox" badge so you can verify your integration works without sending real mail.
  • live — Real sends through AWS SES. Counted against plan limits.

Switching environments is just switching keys — no flag, no different base URL. Test keys start with ck_test_, live keys start with ck_live_ (informational, not enforced).

Scopes

Each API key has one or more permission scopes that control what it can access:

  • full_access — Can access all endpoints. Default for new keys; backwards-compatible with existing keys.
  • sending — Can send emails (POST /v1/send), render templates (POST /v1/render), schedule, cancel, reschedule, manage contacts, manage suppressions. Cannot read delivery status or queue stats.
  • reading — Can check delivery status (GET /v1/send/:jobId), list scheduled emails, view queue stats. Cannot send or modify.

Choose the most restrictive scope that fits your use case. A billing service that only sends transactional emails should use a "sending" key; a monitoring dashboard that checks delivery status should use a "reading" key.

If a key lacks the required scope, the API returns 403 with a structured error including the required_scopes, your key's current scopes, and a hint on how to fix it.

Idempotency

POST /v1/send and POST /v1/batch accept an Idempotency-Key header for safe retries:

Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000

The response is cached for 24 hours — retries with the same key return the cached response without re-sending. Use any unique string (UUIDs work great). The key is scoped to the API key used, so different keys never collide.