Skip to main content
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.

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:all scope, and your account ID

Install the SDK

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. Add sandbox: true to the send request to validate it 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

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 — retried 6 times over 16+ minutes, with the webhook disabled after 100 consecutive failures — 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

  • Templating: pass substitutions per recipient and use {{ variable }} in the subject or body.
  • Batch sends: recipients accepts up to 100 entries; each gets a separate, individually-substituted message.
  • Scheduling: set schedule: { first_attempt: new Date(Date.now() + 60_000).toISOString() } to defer delivery.
  • Your own idempotency keys: pass { idempotencyKey: "order-123" } as the second argument to send() and reuse it only for the same operation and exact request payload.
  • Attachments: pass attachments: [{ data, content_type, file_name, base64: true }]. Set base64: true for binary files such as PDFs.
See the API reference for every endpoint the SDK exposes. For deployment, follow Hono’s Node.js build and deployment guidance, serve the app over HTTPS, and configure your platform’s PORT, secrets, and public webhook URL. Running Hono on Bun instead? The Bun guide covers env loading and the Bun entry point.

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.