Skip to main content
This guide shows how to use AhaSend to send transactional email from Hono in Node.js, keep credentials on the server, and test a sandbox send. This guide builds a Hono server on Node.js and adds a protected email endpoint plus signed webhook handling. Hono also supports other runtimes, whose entry points and secret bindings differ; see the Bun guide or Cloudflare Workers guide for those platform-specific details. Use a send-only key from Credentials → Add → API Key v2 with messages:send:{your-domain}. Use messages:send:all when sending from many domains. For management tasks such as creating a webhook, use a separate full key from Account Settings → API Keys with the needed scopes. See send-only credentials and authentication. Keep credentials on the server. Use sandbox mode for tests. Reuse a stable, business-event idempotency key only with the exact same payload; use different keys for sandbox and live sends. Check every recipient’s status and return a failure when any item is error; HTTP 202 alone does not mean every recipient was accepted. For webhooks, verify the raw request bytes using the full secret as the raw UTF-8 HMAC key, including its aha-whsec- or aha-rsec- prefix. Never base64-decode the secret. The AhaSend SDK verifiers handle this. These examples use a 30,000,000-byte webhook body limit; a hosting platform may impose a smaller limit. Acknowledge verified unknown event types with 2xx. See verification and retry policy.

Prerequisites

  • Node.js 22 or newer (the SDK’s minimum)
  • An AhaSend account with a verified sending domain
  • An API key with the messages:send:{your-domain} scope (messages:send:all for sending from many domains), and your account ID

Install the SDK

Try a Sandbox Send

Run this short SDK check locally before adding the longer server integration below. Use a verified sender, a send-only API key, and your account ID. It sends to a reserved test address with sandbox: true, so no email is delivered. In the server project directory, add .env to .gitignore, then create .env with AHASEND_API_KEY, AHASEND_ACCOUNT_ID, and AHASEND_FROM (an address on your verified domain). Keep these values out of browser code and AI prompts. Use Node.js 22 or newer. Save this as quick-send.mjs:
quick-send.mjs
Run node --env-file=.env quick-send.mjs from that directory. The output lists each recipient status; queued or scheduled means accepted for sandbox processing. The process exits with a failure if the request or any recipient fails. Continue below for the full integration. On Bun you don’t need @hono/node-server: Bun.serve runs Hono natively via export default app.

Configure Environment Variables

Add your credentials to .env (and load them with node --env-file=.env; Bun loads .env automatically):
.env

Create the Client

Create the client once at module scope and reuse it across requests:
lib/ahasend.ts

Send an Email from a Hono Route

server.ts
Call the route from a trusted backend with Authorization: Bearer <WELCOME_ROUTE_TOKEN>, and use a stable eventId for the business event that should send exactly one welcome email. Do not expose this route token to browser code, and put rate limiting in front of the route at your proxy or gateway: the token is the only thing between a caller and your sending quota, and the route will mail any address it is handed. Replace the token check with your application’s normal authentication and authorization if the endpoint is user-facing. 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 SDK retries transient failures automatically. The stable key in the example is reused when your application retries the same business event; reuse it only with the exact same message payload. A key can expire, so it does not replace application-level state that records whether the welcome email was sent. The send example uses sandbox: true to validate the request without delivering anything. sandbox is part of the request body, and the server matches an idempotency key against a hash of that body, so a key already used for a sandbox send is rejected with a 422 when the same key is replayed for the live send — give the two runs different keys.

Handle Webhooks

nextRouteHandler is the SDK’s web-standard Request/Response adapter, so it works with Hono’s c.req.raw. Pass Hono’s raw Web Request to the SDK’s web-standard adapter. The adapter reads the original bytes, enforces the configured size limit, verifies the signature and timestamp, parses the typed event, and returns opaque 400 or 413 responses for invalid or oversized deliveries:
server.ts
Add this route before the serve() call. Signature timestamp checking does not prevent a valid delivery from being replayed within the accepted window. Before performing a side effect, atomically store the verified webhook-id — read from the handler’s second Request argument, as above — together with durable work in one transaction, acknowledge an ID that transaction already holds without re-enqueueing it, and make the work itself idempotent. Let a failed commit reject: the adapter rethrows a handler error, Hono answers 500, and AhaSend retries. Never catch it into a 200. Every non-2xx answer counts as a failed delivery — delivered with 6 attempts over about 16 minutes, with the webhook disabled after more than 100 consecutive failed attempts — so the opaque 400 the adapter returns for a bad signature quietly spends that budget on every delivery until the secret is fixed. Create the webhook in your AhaSend dashboard pointing at https://your-app.com/webhooks/ahasend, and copy its secret into AHASEND_WEBHOOK_SECRET exactly as shown (including the aha-whsec- prefix).

Going Further

For substitutions, attachments and scheduled sends, use the sending guide. The Node.js SDK and Go SDK references explain client options, per-recipient results and retries. Use idempotency rules when your application retries a business operation.

Troubleshooting

Verify AHASEND_WEBHOOK_SECRET matches the dashboard value exactly (including the aha-whsec- prefix), and make sure nothing consumes or rewrites the request body before ahasendWebhook: even reformatting the JSON invalidates the HMAC.
The API key is missing, malformed, or revoked. Verify that AHASEND_API_KEY is loaded without logging any part of it. On Node, remember to start with node --env-file=.env.
The from address must belong to a verified sending domain on your account. Check domain status in the dashboard.