Skip to main content
Keep your AhaSend credentials in SvelteKit’s private environment modules and send email only from server endpoints or server form actions.

Prerequisites

  • A SvelteKit project
  • An AhaSend account with a verified sending domain
  • An API key with the messages:send:{yourdomain.com} scope for that domain (or messages:send:all), and your account ID

Install the SDK

Configure Environment Variables

Add your credentials to .env for local development. The route token protects the server-to-server example below; generate at least 32 random characters for it, because that token is the only thing standing between the send endpoint and an open mail relay.
.env
$env/dynamic/private exposes the private runtime variables supplied by your deployment platform. With adapter-node, these are equivalent to process.env. Configure the same variables in your deployment platform for production. Values imported from $env/static/private are instead injected at build time.
Never prefix these values with PUBLIC_ or import the client into browser code. Rotate an API key immediately if it is exposed.

Create the Client

Build the client on first use and reuse it across requests. SvelteKit prevents $lib/server/ modules from being imported into client code, so keep it there:
src/lib/server/ahasend.ts
Read the variables inside the function rather than while the module is evaluating. $env/dynamic/private is resolved by the platform at runtime, so what it holds during a module’s first evaluation depends on the adapter — a module-scope assertion turns a missing value into a failure of the whole route module instead of a failure of one request.

Send an Email from a SvelteKit Endpoint

This example is a server-to-server endpoint. It authenticates the caller before reading a bounded request body and requires a stable event ID for safe retries. AhaSend rate-limits message operations per account (100 requests per second, with a 200-request burst), so also put a request-rate limit in front of this route: an authenticated caller must not be able to spend the whole account budget.
src/routes/api/welcome/+server.ts
An accepted send is multi-status: result.data contains one result per recipient, and an entry can have status: "error" even though the request resolved. Reuse the same eventId when retrying the same business operation; the stable idempotency key lets AhaSend replay a stored outcome instead of creating another send. A server-error outcome can be re-executed, so make the surrounding business workflow tolerate an uncertain duplicate. Do not expose or log provider errors, recipients, message content, or the idempotency key. A rate-limit error surfaces only after the SDK has already retried it while honouring Retry-After, so pass that budget on to the caller instead of flattening it into a generic failure. For a browser submission, prefer a SvelteKit form action. Authorize it with the user’s server-side session and derive the recipient on the server instead of trusting a browser-supplied address. Add sandbox: true to validate a send without delivering it. Idempotency keys are scoped to the account and matched against a hash of the request body, so a sandbox send is not a separate namespace: flipping sandbox while reusing welcome:<eventId> is the same key with a different payload, which the API rejects with a 422 for the 24 hours the original record lives. Give sandbox sends their own key prefix.

Handle Webhooks

nextRouteHandler is the SDK’s adapter for web-standard Request/Response handlers, so it works unchanged in a SvelteKit +server.ts. It reads a bounded raw body and verifies the signature over those exact bytes before invoking your handler:
src/routes/api/webhooks/ahasend/+server.ts
Pass the webhook secret exactly as the dashboard displays it, including the aha-whsec- prefix. The adapter preserves the exact signed bytes, returns an opaque error for an invalid or oversized request, and keeps unknown but valid events on the success path. maxBodyBytes can only narrow the SDK’s own ceiling; it cannot raise your platform’s. adapter-node rejects a body over BODY_SIZE_LIMIT — 512kb by default — before your handler runs, so raise that variable to match the bound you choose here, or lower maxBodyBytes to match it. Otherwise the platform produces the rejection and the SDK’s bound never applies. Before doing business work, atomically claim the verified request’s webhook-id header in durable storage and enqueue the work in the same transaction (or use a durable outbox). Acknowledge an already claimed ID and make downstream processing idempotent. Timestamp validation alone does not prevent replay within the accepted window. Commit that claim and enqueue before returning the response. A promise left running after the handler resolves has no guarantee of completing — serverless adapters may freeze or discard the invocation as soon as the response is sent, which acknowledges a delivery whose work never ran and which AhaSend will therefore never retry. SvelteKit’s CSRF origin check applies only to POST, PUT, PATCH, and DELETE requests whose content type is application/x-www-form-urlencoded, multipart/form-data, or text/plain, so a JSON webhook delivery reaches this route untouched. The check is enforced in production but not in local development, so any cross-origin caller that does use one of those three content types belongs in kit.csrf.trustedOrigins rather than being handled by turning checkOrigin off. Create the webhook in your AhaSend dashboard, point it at https://your-app.com/api/webhooks/ahasend, and configure its secret as AHASEND_WEBHOOK_SECRET.

Going Further

  • Deployment: choose a server-capable SvelteKit adapter and configure all private environment variables in its deployment platform. A static build cannot run these endpoints.
  • 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.
  • Attachments: pass attachments: [{ data, content_type, file_name, base64: true }]; binary data must be base64 encoded.
See the API reference for every endpoint the SDK exposes. The same server-side pattern works in the other meta-frameworks: see the Next.js and Nuxt guides.

Troubleshooting

You imported the AhaSend client (or an environment module) from a component or +page.ts that runs in the browser. Move all SDK usage into +server.ts, +page.server.ts, or $lib/server/ modules. SvelteKit enforces this boundary.
Rotate the exposed key in the dashboard. Remove any PUBLIC_ prefix and read the replacement only from $env/dynamic/private or $env/static/private in server-only code.
Confirm that the deployment uses a server-capable adapter rather than a static build and that its runtime has all four private environment variables configured.
The API key is missing, malformed, or revoked. Confirm that AHASEND_API_KEY and AHASEND_ACCOUNT_ID are configured in the production environment and that the key still exists in the dashboard.
The key authenticated but lacks the scope for this operation — for a send, that is usually sender domain not found in api key scopes. Grant the key messages:send:{yourdomain.com} for the domain in from.email, or messages:send:all.