Authentication
Nerve has two authentication mechanisms, because it has two kinds of caller.
| Who is calling | How they authenticate | Which routes |
|---|---|---|
| Your application | Authorization: Bearer <API key> | Everything under /v1/ except health and webhooks |
| Your providers | Per-provider HMAC signature | POST /v1/webhooks/{provider} |
| Nobody / your load balancer | No auth | GET /v1/health |
Bearer API keys
Send your workspace API key in the Authorization header on every request:
curl https://api.nervly.io/v1/events/trigger \
-H "Authorization: Bearer nerve_sk_live_a1b2c3d4e5f6..." \
-H "Content-Type: application/json" \
-d '{ ... }'
A missing, malformed, or unrecognised key returns 401 Unauthorized:
{
"error": "UNAUTHORIZED",
"message": "Missing or malformed Authorization Bearer header",
"status_code": 401
}
Live vs. Test API keys
Nerve issues two modes of API keys:
| Key Prefix | Mode | Delivery Behaviour | Cost |
|---|---|---|---|
nerve_sk_live_... | Live | Dispatches to real external providers (SMS, Email, Push, WhatsApp) | Real provider cost |
nerve_sk_test_... | Test | Short-circuits provider execution; emits synthetic nerve_test_{event_id} | Zero |
In Test Mode:
- Zero outbound network calls are made to external providers.
- Messages and timelines are still recorded normally in PostgreSQL and can be inspected in the dashboard.
- Safe for local integration tests, staging environments, and CI pipelines without running up provider bills or sending accidental OTPs to real users.
Security and verification guarantees
- Constant-Time Comparison: The gateway resolves keys using constant-time comparison (
subtle::ConstantTimeEqHMAC-SHA256) to eliminate timing side-channel attacks. - One-Time Reveal: When created in the dashboard, the raw API key is displayed once in a modal. Only a cryptographic hash (
SHA-256) is stored in the database. - Sub-millisecond Key Lookups: Resolved keys are cached briefly to keep high-throughput ingest off the database, keyed by a non-reversible digest of the credential.
- Immediate Revocation: Revoking a key in the dashboard invalidates the cache entry at once. In-flight requests using the revoked key fail immediately with
401.
Webhook signatures
Your providers cannot present your API key, so
POST /v1/webhooks/{provider}
sits outside the bearer-token surface and authenticates each provider on its
own terms:
- Termii: Signs the raw request body with HMAC-SHA256 under your Termii webhook secret and sends the hex digest as
X-Termii-Signature. - Paystack: Inbound payment webhooks verify the
x-paystack-signatureHMAC-SHA512 header over the raw body before parsing JSON. - Twilio / SendGrid / Postmark: Verified using provider-specific signature headers and token validation.
Health checks
GET /v1/health needs no authentication, by design — a load balancer should
not need a secret to decide whether to route traffic. It exposes the running
service version, environment, and broker connectivity:
curl https://api.nervly.io/v1/health
{
"status": "OK",
"service": "nerve-gateway",
"version": "0.1.0",
"environment": "production",
"uptime_seconds": 86400,
"nats_connected": true
}