secret() rather than process.env, and the webhook endpoint must be declared raw, since signature verification needs the unparsed body.
Prerequisites
- Node.js 22 or newer (the SDK’s supported floor) and the Encore CLI
- An Encore app with a service: the files below live in a service directory (
email/, alongside itsencore.service.ts) - An AhaSend account with a verified sending domain
- An API key with the domain-scoped
messages:send:{yourdomain.com}permission, and your account ID
Install the SDK
Store Credentials as Encore Secrets
Encore has first-class secrets instead of.env files. Set each secret for local development (and again with --type prod for production):
local (and any preview) environment a dedicated sandbox-mode API key and its own webhook secret. A sandbox credential simulates the whole pipeline without delivering, so encore run on a developer’s machine cannot mail a real customer even when the code forgets to ask for it.
In code, declare secrets with secret() from encore.dev/config. Each declaration returns a function you call to read the value:
email/ahasend.ts
Send an Email from an Encore API Endpoint
email/welcome.ts
Promise<SendWelcomeResponse> annotation is load-bearing: Encore derives the endpoint’s response schema from the handler’s declared return type, not from what it actually returns, so an unannotated handler compiles into an endpoint that answers with an empty body.
This assumes your app has an Encore auth handler whose auth data contains immutable userID and verified email fields. Taking the recipient from authenticated server data prevents the endpoint from becoming an arbitrary-recipient mail relay. A 202 is multi-status: result.data carries one entry per recipient, and an individual recipient can come back status: "error" with a null id (a suppressed address, say) while the call itself succeeds, so check every entry, not just the first.
The business-stable idempotency key lets a retried signup reuse the same operation. Use the same key only for the exact same payload. AhaSend retains ordinary idempotency results for 24 hours, but server-error outcomes are not stored and a retry can re-execute the send, so this is not an exactly-once guarantee. Persist a completed-welcome marker in your application database when duplicate prevention must last longer, and make the workflow safe to reconcile after an uncertain result.
Run it locally:
sandbox: true to the send request to validate it without delivering anything. Give that trial its own idempotency key: AhaSend matches a key against a hash of the request body, so a sandbox call under the welcome- key makes the first real send for that user fail with 422 instead of delivering.
Handle Webhooks with a Raw Endpoint
Signature verification needs the raw request body, which Encore’s typed endpoints parse away. Useapi.raw instead: its handler receives a Node IncomingMessage, so you collect the body chunks into a Buffer and hand them to the verifier along with the headers. verifier.parse() is asynchronous, so await it:
email/webhooks.ts
webhook-id header together with durable queue/outbox work; acknowledge an already-committed ID with 2xx, and process the durable work idempotently. Timestamp verification alone does not prevent a valid delivery from being replayed inside the tolerance window. bodyLimit is Encore’s own cap for this endpoint — it defaults to 2 MiB when left unset — and the streaming counter bounds what the handler buffers even if that cap is later raised; an oversized body is refused either way. Keep any proxy in front at least as strict, and keep failures opaque. The verifier is rebuilt when the secret value changes, for the same reason the API client is: a webhook secret rotated in the dashboard would otherwise leave a stale verifier rejecting every delivery as 400 until the next deploy, and AhaSend gives up on an event after 6 retries and disables the endpoint after 100 consecutive failures.
Create the webhook in your AhaSend dashboard pointing at https://your-app.com/webhooks/ahasend (locally, encore run serves it at http://localhost:4000/webhooks/ahasend), and set the secret via encore secret set exactly as shown in the dashboard, including the aha-whsec- prefix.
Not on Encore everywhere? The same SDK, including dedicated webhook adapters, powers the Express and Fastify guides.
Going Further
- Templating: pass
substitutionsper recipient and use{{ variable }}in the subject or body. - Batch sends:
recipientsaccepts up to 100 entries; each gets a separate, individually-substituted message. - Tags: pass
tagson the send to group messages for filtering and statistics later. - Secrets for everything: the webhook secret belongs in
encore secret setalongside the API key, so neither value lives in your repo or in a.envfile. - Attachments: pass
attachments: [{ data, content_type, file_name, base64: true }]. Setbase64: truefor binary files such as PDFs.
Troubleshooting
Secret not found when calling the endpoint
Secret not found when calling the endpoint
Secrets are per-environment. If it works locally but fails when deployed, you set
--type local but not --type prod (or the environment type you deployed to).401 AhaSendAuthenticationError
401 AhaSendAuthenticationError
The
AhaSendApiKey secret holds a missing, malformed, or revoked key. Re-set it and verify the key exists in your dashboard.Webhook verification always returns 400
Webhook verification always returns 400
Either the endpoint isn’t
api.raw (typed endpoints consume the raw body), or AhaSendWebhookSecret doesn’t match the dashboard value exactly.
