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 (ormessages: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.
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
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
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
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
substitutionsper recipient and use{{ variable }}in the subject or body. - Batch sends:
recipientsaccepts 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.
Troubleshooting
Cannot import $env/static/private into client-side code
Cannot import $env/static/private into client-side code
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.API key visible in the browser bundle
API key visible in the browser bundle
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.Server endpoint is missing after deployment
Server endpoint is missing after deployment
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.
401 from AhaSend
401 from AhaSend
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.403 from AhaSend
403 from AhaSend
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.
