Skip to main content

Authentication

Nervly has two authentication mechanisms, because it has two kinds of caller.

Who is callingHow they authenticateWhich routes
Your applicationAuthorization: Bearer <API key>Everything under /v1/ except health and webhooks
Your providersPer-provider HMAC signaturePOST /v1/webhooks/{provider}
Nobody / your load balancerNo authGET /v1/health

Bearer API keys​

Send your workspace API key in the Authorization header on every request. Every example below sends the same authenticated POST /v1/events/trigger:

curl -X POST https://api.nervly.io/v1/events/trigger \
--connect-timeout 5 \
--max-time 10 \
-H "Authorization: Bearer $NERVLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "order-confirmed",
"to": { "subscriberId": "sub_4920a", "phone": "+2348012345678" }
}'

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​

Nervly issues two modes of API keys:

Key PrefixModeDelivery BehaviourCost
nervly_sk_live_...LiveDispatches to real external providers (SMS, Email, Push, WhatsApp)Real provider cost
nervly_sk_test_...TestShort-circuits provider execution; emits synthetic nervly_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.

Key scopes​

API keys carry read and/or write scopes:

  • read lets the key call the diagnostic and read endpoints, and the read-only MCP tools: gateway_status, idempotency_inspect, check_delivery_status, list_templates and verify_subscriber_channel.
  • write lets the key send — POST /v1/events/trigger, POST /v1/events/bulk, subscriber preference updates and subscriber deletion — and call send_notification over MCP.
  • write implies read. When you omit the choice, the key gets both.

A read-only key cannot send notifications. The gateway omits send_notification from tools/list for such a key and rejects a direct call to it. See Managing API keys for how to create each kind.

Security and verification guarantees​

  1. Constant-Time Comparison: The gateway resolves keys using constant-time comparison (subtle::ConstantTimeEq HMAC-SHA256) to eliminate timing side-channel attacks.
  2. 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.
  3. 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.
  4. 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: HMAC-SHA256 over the raw request body, hex-encoded in X-Termii-Signature.
  • Resend: Svix-signed — HMAC-SHA256 over {svix-id}.{svix-timestamp}.{body}, base64 in svix-signature.
  • ZeptoMail: HMAC-SHA256 over the raw request body, base64 in producer-signature.
  • Infobip: HMAC-SHA256 over the raw request body, base64 or hex in X-Hub-Signature.
  • Twilio: Twilio's own request signature (HMAC-SHA1 over the request URL and its sorted form parameters).

Setup, encodings, replay windows, and copy-pasteable verifiers in ten languages are on Delivery receipts. Billing webhooks from Paystack are a separate surface — see Quotas and billing.

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": "nervly-gateway",
"version": "0.1.0",
"environment": "production",
"deployment": "primary",
"key_mode": "any",
"uptime_seconds": 86400,
"nats_connected": true
}

Next​