Skip to main content
@ahasend/sdk is the official Node.js and TypeScript SDK for AhaSend. Typed models, retries that carry an idempotency key, and a separate webhooks entry point that verifies signatures for you. This page is the SDK reference. To wire it into a specific framework, pick a guide from the sidebar.

Install

Runtime Compatibility

TypeScript declarations ship with the package, so there is no SDK-specific @types package to add. Resolve them with moduleResolution set to bundler, node16, or nodenext; the package exports map is what makes @ahasend/sdk/webhooks resolvable. The declarations also name four WHATWG globals a fetch client cannot hide — fetch, Request, Response, and AbortSignal — so your project needs either @types/node or "lib": ["DOM"]. With neither, compilation fails inside node_modules rather than in your own code.
Browsers and browser Service Workers are refused, because the API key grants full send access and anything the browser can read, a visitor can read. dangerouslyAllowBrowser: true exists only for browser-shaped server test environments.

Send Your First Email

Create a least-privilege API key with messages:send:{your-domain}, and keep the key and account ID in an uncommitted .env file or your deployment secret store.
send.mjs
Run it with node --env-file=.env send.mjs. Change sandbox to false only after you have intentionally reviewed the sender, recipients, and production credentials. Note what the result handling is doing. recipients takes up to 100 addresses and each one becomes its own message, so result.data holds one entry per recipient. A single recipient can come back status: "error" with a null id, a suppressed address for instance, while the call itself succeeds. Reading data[0] would miss that. Body fields are snake_case, matching the API. Beyond the ones above: reply_to, attachments, headers, substitutions, tags, tracking, retention, and schedule. For one conversation message with visible To and Cc recipients and hidden Bcc recipients, use messages.sendConversation() instead.
sandbox_result rehearses a specific outcome: deliver, bounce, defer, fail, or suppress. Each one fires the matching webhook, so you can test your bounce handling without a real bounce. See Sandbox Mode.

Configure the Client

Build the client once at module scope and share it. Client-level retry, telemetry, idempotency, and local rate-pacing configuration then stays consistent, and any enabled pacing queue is shared by your handlers in that process.
lib/ahasend.ts
AhaSendClient.fromEnv() builds the same client from AHASEND_API_KEY (or AHASEND_TOKEN) and AHASEND_ACCOUNT_ID, along with the other AHASEND_* variables. It reads process.env by default and accepts any string record instead, which is how runtimes that hand you bindings rather than a process environment — Cloudflare Workers, for one — supply the same variables: AhaSendClient.fromEnv(env). Every method also takes a trailing options object:

Errors

Every non-2xx response throws a typed error:
AhaSendError is the root. AhaSendAPIError carries .status, .code, .requestId, and .body, and branches into AhaSendBadRequestError, AhaSendAuthenticationError, AhaSendPermissionError, AhaSendNotFoundError, AhaSendConflictError, AhaSendIdempotencyConflictError, AhaSendUnprocessableEntityError, AhaSendIdempotencyMismatchError, AhaSendRateLimitError, and AhaSendServerError. Alongside it sit AhaSendConnectionError with its AhaSendTimeoutError subclass, plus AhaSendAbortError, AhaSendConfigurationError, AhaSendRateLimitQueueFullError, AhaSendResponseTooLargeError, and AhaSendResponseParseError. Match on error.code or error.status, never on message text. Logs, metrics, traces, and exception tags should allowlist only aggregate counts, appropriate opaque IDs, HTTP status, SDK error code, and request ID. Do not serialize whole requests, responses, events, or errors, and do not log .body, .message, credentials, addresses, content, or idempotency keys.

Retries and Idempotency

The SDK retries only when the generated operation profile marks the call safe, idempotent, or protected by an idempotency key. For an eligible call it retries 408, 429, 5xx, network failures, timeouts, and an idempotency-in-progress 409 carrying the required replay and retry headers. Caller cancellation and other 4xx responses are terminal. Create operations declared idempotent receive an automatically generated UUID Idempotency-Key, which is reused across the logical call’s internal retries.
Stored non-secret outcomes replay for 24 hours, covering 2xx and deterministic 4xx responses. The two API-key create operations are the exception: their replay carries the same one-time secret_key for only 5 minutes, so persist that value the moment it arrives. Server errors are not stored, so the same key can execute again after a 5xx. Pass your own idempotencyKey derived from a stable business identifier when your application may retry later, reuse it only with the exact same payload, and reconcile an uncertain result before another send when duplicates are unacceptable.
Local rate pacing is separate and off by default. Turn it on with rateLimit: { enabled: true }.

Resources

List endpoints paginate. Take a page at a time with list(), or let iterate() walk everything lazily:

Webhooks

@ahasend/sdk/webhooks is a separate entry point holding the Standard Webhooks HMAC-SHA256 verifier, typed parsers for all 11 event types, and adapters for Express, Fastify, and Next.js. It does not pull in the API client.
parse() and verify() are asynchronous. Await them before treating the body as trusted. The Express, Fastify, and Next.js adapters await verification internally.
Verification needs the raw request body, the exact bytes AhaSend sent. If a JSON body parser runs first the signature can no longer be checked: expressWebhookHandler hands that to next, fastifyWebhookHandler answers an opaque 400, and nextRouteHandler reads and bounds the stream itself. Pass the webhook secret exactly as the dashboard shows it, aha-whsec- prefix included. Every adapter answers a failed verification with an empty 400, or 413 when the body was too large, so nothing about the failed check reaches the sender. Timestamp checking rejects any delivery more than five minutes from your clock, adjustable with new WebhookVerifier(secret, { toleranceSeconds }), but it does not deduplicate a valid one replayed inside that window. After verification, atomically commit each webhook-id together with durable work or an outbox record. A unique ID insert by itself can lose an event if the process stops before performing the side effect. Acknowledge duplicates without enqueuing again, retry storage failures with a non-2xx response, and process the durable work idempotently outside the request. Direct verification and every adapter enforce a fixed 30,000,000-byte body ceiling. Adapters buffer the whole body before verifying it, and decoding it costs more memory again, so narrow that ceiling to what your payloads actually need: maxBodyBytes is a trailing option on expressWebhookHandler, fastifyWebhookHandler, and nextRouteHandler, and it only lowers the fixed limit, never raises it. WebhookVerifier takes no such option — on the direct path above, bound the read that produces rawBody yourself. Either way, set matching request-size and concurrency limits at the reverse proxy.

Framework Guides

Express

Next.js

Fastify

NestJS

Nuxt

SvelteKit

Every other framework, runtime, and platform is in the sidebar. Building in Go instead? See the Go SDK.

Source and Support

MIT licensed, developed at github.com/AhaSend/ahasend-ts. Report reproducible bugs as GitHub issues, without credentials, message content, or webhook payloads.