Skip to main content
Claude Code can run the code it writes after you approve the command and permit the required network access, so it can send a test message and read the response back. That turns plausible code into observed-working code, as long as you hand it the real API first.

Prerequisites

Claude can write and run code, but it can’t verify your domain or mint your credentials. Do these two things in the AhaSend dashboard first:
  1. Verify a sending domain. Add the SPF, DKIM, and DMARC records from the domain setup guide. Unverified domains cannot send and produce an API error.
  2. Create an API key. Follow the API credentials guide and scope it to messages:send:{your-domain} rather than messages:send:all: if it leaks, the blast radius is one domain’s mail, not your whole account. Grab your account ID too.
Put both in your environment as AHASEND_API_KEY and AHASEND_ACCOUNT_ID. If you use the .env file shown later, create it yourself, ensure .env is ignored by git before adding the values, and do not paste the values into Claude or ask it to read or echo the file. Claude Code persists conversations and tool activity locally. You’ll also need Node.js 22 or newer, the SDK’s supported floor.

Step 1: Install the SDK

Do this before you prompt. An agent that finds the dependency already present has far less room to improvise.

Step 2: Write a CLAUDE.md With the Real API Facts

Ask Claude cold and you’ll get a client from a package nobody has published. Give it the ground truth as standing context instead. In Claude Code, that context lives in a CLAUDE.md file at the root of your project, which Claude reads at the start of every session:
CLAUDE.md
Claude Code now loads these project instructions each session, but instructions are context rather than a guarantee, so review the generated code against them. In the Claude app, paste the same facts into your message, or, when web search and web fetch are enabled, point Claude at AhaSend’s machine-readable doc index at https://ahasend.com/docs/llms.txt. The equivalent for an AI editor is a rules file, as in the Cursor guide.

Step 3: Ask for the Whole Workflow

Don’t ask for a send, ask for the flow, and name the steps:
“Build a signup-confirmation email workflow. When a new user is created, commit the user and a uniquely keyed welcome-email job in one transaction, then send that job from a worker with the AhaSend SDK, using substitutions for their first name and a stable idempotency key derived from the job. Take the recipient address from the stored user record, never from a request body, and never fire the send as a detached promise inside the request handler. Add a webhook endpoint that verifies AhaSend events with @ahasend/sdk/webhooks, deduplicates each verified webhook-id atomically with durable work, and marks bounced addresses as undeliverable. Read credentials from the environment per CLAUDE.md. Build it in sandbox mode and run it to confirm it works before telling me it’s done.”
For a multi-file change like this, switch Claude Code into Plan mode first. It researches and proposes an approach before writing anything, so you can sign off on the shape before a line of code exists: a plan that separates the send, the template, and the bounce handler instead of one tangled function. Here is the send the plan should produce:
lib/send-welcome.ts
The substitutions object feeds AhaSend’s Jinja-style templating, so {{ first_name }} resolves per recipient, which turns a one-off send into a reusable workflow step.

Step 4: Run It in Sandbox Mode

Claude can execute the script it just wrote after you approve the command and allow outbound HTTPS to api.ahasend.com. Ask it to add a small runner and then run it.
scripts/send-sandbox.mjs
Run it with node --env-file=.env scripts/send-sandbox.mjs. The message goes through validation and processing, triggers relevant configured webhooks, costs nothing, and is never delivered. Claude runs the command, reads the output, and confirms the send was accepted. To verify the handler too, first configure an AhaSend webhook that subscribes to bounce events and points to a URL AhaSend can reach; for local development, expose the handler through a tunnel. Copy the webhook secret into your environment as AHASEND_WEBHOOK_SECRET exactly as shown, including its aha-whsec- prefix, without pasting it into Claude. Then sandbox_result: "bounce" triggers the bounce webhook, and Claude can check the durable result after it arrives. Every outcome is listed in the sandbox mode guide, and each one triggers the matching webhook, so the whole reachable pipeline gets tested, not just the happy path. Add this rule to CLAUDE.md: default sandbox to true everywhere except production, and drive it from an environment variable so going live is a config change, not a code edit.

Step 5: Review the Diff

Confirm four things before you merge, even though Claude ran its own tests:
  • The trigger isn’t a mail relay. Find every path that can cause a send and check it is authenticated, validated, and rate-limited, with the recipient address read from your stored user record rather than the request body. A route that mails whatever address a caller posts lets strangers send from your verified domain. Check too that the send is handed to a durable job instead of being detached from the request, where the runtime can kill it after the response.
  • The real package. The TypeScript import must be @ahasend/sdk. Go uses github.com/AhaSend/ahasend-go; there is no Python SDK yet, so do not invent one—use the REST API from Python.
  • Every recipient checked. A promise that resolves means the request was accepted, not that every address was. Code reading result.data[0] and moving on will silently miss a suppressed recipient.
  • The key lives in the environment. Agents that touch many files can drift a secret into the wrong one. Confirm the key is read from process.env, never written to anything git tracks, and never assigned to a client-exposed variable (NEXT_PUBLIC_, VITE_, PUBLIC_, NUXT_PUBLIC_), which ships it to the browser.
Then merge, set AHASEND_API_KEY, AHASEND_ACCOUNT_ID, and AHASEND_WEBHOOK_SECRET in your hosting platform’s secret store, turn sandbox off in production, and send one message to an address you control.
Webhook signature verification needs the raw request body. If Claude mounted a global JSON parser in front of the webhook route, verification will fail no matter how correct the secret is. The Express guide shows the correct mounting order.

Going Further

  • Extend the loop to failure: with a configured, reachable webhook, the same sandbox workflow can prove the bounce path. Ask Claude to drive sandbox_result through bounce and suppress and assert on what your code does next.
  • Templating and webhooks: per-recipient substitutions cover personalised batches, and @ahasend/sdk/webhooks verifies delivery-event signatures. Both belong in CLAUDE.md so the next workflow starts from them.
  • Same grounding trick, different file: Cursor reads .cursor/rules/, GitHub Copilot reads .github/copilot-instructions.md, and Windsurf reads .windsurf/rules/.
  • For the finished shape of the integration Claude is writing, see Express, Next.js, or NestJS.

Troubleshooting

Add @ahasend/sdk to package.json yourself before prompting. Run /context and check Memory files to confirm the intended CLAUDE.md loaded; use /memory to inspect or edit the configured files. Start a new session if you created the file after the current session began.
Check the Node runtime in the terminal Claude is using, not just your shell, since a version manager can leave the agent on a different runtime than your normal session.
Sandbox sends fire the matching webhook, but only if a webhook is configured in the dashboard and pointed at a URL AhaSend can reach. During local development, expose the endpoint through a tunnel and check the secret is copied exactly, including the aha-whsec- prefix.
That’s the multi-status response working as designed: the request was accepted, that address was not. The most common cause is a suppressed address from an earlier hard bounce. Inspect result.data entry by entry and check the suppression list in the dashboard.
Every endpoint the SDK exposes is in the API reference.