Vetrix Docs

Admin Email API

Two admin-only endpoints under /admin/settings diagnose SMTP regressions without a manual psql query against email_deliveries:

  • GET /api/v1/admin/email/deliveries/health
  • POST /api/v1/admin/email/test

Both endpoints live under the existing admin:settings scope.

Authentication

  • Bearer JWT required. Anonymous returns 401.
  • Caller must be a super-admin or hold the admin:settings grant.
  • Non-admin returns 403 with the missing scope in the body.

Rate limits

Two layers apply:

  1. Standard RateScope budget: api.read for health, api.write for test. Configurable per user group via the rate-limit config.
  2. Per-admin floor on the test endpoint: 1 request per 10 seconds per actor. Independent of the group budget. A second call within the cooldown returns 429 with a Retry-After header (in seconds).

GET deliveries health

Path: GET /api/v1/admin/email/deliveries/health

Returns one summary row per distinct template_slug seen in email_deliveries within the last 30 days. Used by the admin email settings page to surface lines like "the verification template has 17 failures in the last 24h with last_error = 535 5.7.8" without a manual psql round-trip.

Response shape (200):

  • window_start: ISO8601 RFC3339 lower bound of the 30-day window.
  • generated_at: ISO8601 RFC3339 when the snapshot was computed.
  • templates: array of per-slug rows with these fields:
    • template_slug
    • latest_status, latest_created_at, latest_sent_at, latest_attempts, latest_recipient
    • latest_error (truncated to ~250 runes, ellipsis appended)
    • latest_failed_at, latest_failed_error (most recent FAILED row for the slug; independent of the latest_* pointers)
    • failed_24h, sent_24h (fixed last-24h window regardless of outer 30d look-back)
    • queued (point-in-time count of queued rows)

Error codes:

  • 401: no JWT.
  • 403: not an admin or missing admin:settings scope.
  • 503: email store not configured. Transient during boot.
  • 500: DB error reading email_deliveries.

POST test

Path: POST /api/v1/admin/email/test

Performs a synchronous Deliverer.Send against the smtp_test template, addressed to the requested recipient. Returns the inline outcome envelope: success surfaces status = sent; an SMTP-layer failure surfaces status = failed along with the wire-level last_error string. HTTP status is 200 in BOTH cases; the SMTP outcome lives in the body.

Request body: { "recipient_email": "operator@example.com" }

recipient_email is optional. When omitted, the test send goes to the calling admin user email on file (same default as the existing /api/v1/admin/settings/smtp/test endpoint).

Success response (200): { "status": "sent", "recipient_email": "operator@example.com" }

SMTP-failure response (200): { "status": "failed", "last_error": "smtp auth: 535 5.7.8 Error: authentication failed: (reason unavailable)", "recipient_email": "operator@example.com" }

Error codes:

  • 400: no recipient and admin has no email on file; OR recipient failed the cheap-shot validator (missing @, no host dot).
  • 401: no JWT.
  • 403: not an admin or missing admin:settings scope.
  • 429: per-admin rate limit (1 req per 10s). Retry-After header set and retry_after_secs in the body.
  • 503: email deliverer not configured. Transient during boot.

Audit

Every test-send call writes an audit_log row with:

  • action = admin.email.test
  • resource = email:smtp_test
  • actor_id = admin user id
  • details.recipient_email = the recipient
  • details.status = sent or failed
  • details.last_error = truncated error on failure (only)

Audit emit is detached from the inbound request context so a client disconnect mid-send does not lose the row. Audit failures themselves are non-fatal.