> ## Documentation Index
> Fetch the complete documentation index at: https://ahasend.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> AhaSend stores all message data in the EU. See /facts.
> The API base URL is https://api.ahasend.com. Send the API key in the Authorization: Bearer header. See /api-reference/authentication.
> Use send.ahasend.com for SMTP. Ports 25, 587 and 2525 use STARTTLS. Port 465 is not supported. See /smtp.
> For send-only integrations, create an API Key v2 under Credentials → Add and scope it to messages:send:{your-domain}. See /send-api/credentials and /api-reference/scopes.

# How to Add Email to Your App with GitHub Copilot

> Delegate an AhaSend email integration to the GitHub Copilot coding agent: write the issue, review the PR, and test safely in sandbox mode.

This guide shows how to send transactional email from an app built with GitHub Copilot with AhaSend, keep credentials on the server, and test a sandbox send.

GitHub Copilot cloud agent works asynchronously and can open a pull request for your review. It is limited to its working branch and a human must review and merge its pull request. It cannot access Actions, Codespaces, or Dependabot secrets, but it can access any **Agents** secrets that an administrator explicitly gives it.

Use a **send-only key** from **Credentials → Add → API Key v2** with `messages:send:{your-domain}`. Use `messages:send:all` when sending from many domains. For management tasks such as creating a webhook, use a separate **full key** from **Account Settings → API Keys** with the needed [scopes](https://ahasend.com/docs/api-reference/scopes.md). See [send-only credentials](https://ahasend.com/docs/send-api/credentials.md) and [authentication](https://ahasend.com/docs/api-reference/authentication.md).

Keep credentials on the server. Use [sandbox mode](https://ahasend.com/docs/send-api/sandbox.md) for tests. Reuse a stable, business-event idempotency key only with the exact same payload; use different keys for sandbox and live sends. Check every recipient's status and return a failure when any item is `error`; HTTP 202 alone does not mean every recipient was accepted.

For webhooks, verify the raw request bytes using the full secret as the raw UTF-8 HMAC key, including its `aha-whsec-` or `aha-rsec-` prefix. Never base64-decode the secret. The AhaSend SDK verifiers handle this. These examples use a 30,000,000-byte webhook body limit; a hosting platform may impose a smaller limit. Acknowledge verified unknown event types with 2xx. See [verification](https://ahasend.com/docs/api-reference/webhooks/security.md) and [retry policy](https://ahasend.com/docs/api-reference/webhooks/retry-policy.md).

## AhaSend Rules for AI Coding Agents

Add this shared block to .github/copilot-instructions.md:

```markdown theme={null}
## AhaSend rules for AI coding agents

- Use @ahasend/sdk on the server (Node.js 22+, Deno, Bun, or a supported edge runtime).
- Create a send-only API Key v2 under Credentials → Add, scoped to messages:send:{your-domain}.
  Use messages:send:all only for sending from many domains. Management calls need a separate
  full key from Account Settings → API Keys with the needed scopes.
- Never put API keys in browser code, logs, prompts, or source control. Use server secrets.
- Use AhaSendClient with apiKey and accountId, then await client.messages.send(request, options).
  The v2 path is POST /v2/accounts/{account_id}/messages. from is an object, recipients is an array,
  and content fields are text_content and html_content. Use Create Conversation for CC/BCC.
- Authenticate send routes and take recipients and business-event IDs from trusted server data.
  Add request-size and rate limits. Await sending or commit a durable job before returning success.
- Require AHASEND_DELIVERY_MODE to be sandbox or live; reject missing or other values.
  Use a sandbox-mode credential for agent tests. Never decide delivery mode from NODE_ENV.
- Set sandbox from that explicit mode. Prefix stable business-event idempotency keys with the mode.
  Reuse a key only with the same request. Ordinary results last 24 hours; 5xx may re-execute.
- Inspect every result.data item: queued and scheduled were accepted; error was rejected and has
  a null id. Return a non-2xx result or fail the job when any recipient is rejected.
- Verify webhooks with @ahasend/sdk/webhooks before parsing or acting on untrusted data.
  Await verification. Use the exact raw body and the literal UTF-8 bytes of the full secret,
  prefix included; never base64-decode it. Bound the body to 30,000,000 bytes before reading it.
- Return 2xx for verified unknown events. Commit webhook-id and durable work atomically,
  acknowledge committed duplicates, and retry storage failures. The default clock tolerance
  is five minutes. Never log payloads, recipient addresses, signatures, secrets, or idempotency keys.
- Use a verified sender domain. Schedule the first attempt in the future and within 7 days.
  Encode binary attachment bytes as base64 yourself before setting base64: true.
- Check the API and SDK docs instead of inventing packages, fields, event names, or flags.
  Docs index: https://ahasend.com/docs/llms.txt
```

## Prerequisites

Ensure Copilot cloud agent is enabled for the target repository and that you have write access to it. Without both, Copilot cannot be assigned the issue.

The agent can write the integration, but it cannot verify your domain or create your credentials. Do that in the [AhaSend dashboard](https://dash.ahasend.com) first:

1. **Verify a sending domain.** Add the SPF, DKIM, and DMARC records from the [domain setup guide](https://ahasend.com/docs/domains.md).
2. **Create an API key.** Follow the [API credentials guide](https://ahasend.com/docs/send-api/credentials.md) and scope it to `messages:send:{your-domain}` instead of `messages:send:all`. Note your **account ID**.

Keep the deployed application's API key in your hosting platform's secret store. You do not need to expose it to Copilot for the agent to implement and test the integration.

## Step 1: Install the SDK and Commit It

Install the official SDK and commit the manifest and lockfile before assigning the task.

```bash npm theme={null}
npm install @ahasend/sdk
```

```bash pnpm theme={null}
pnpm add @ahasend/sdk
```

```bash yarn theme={null}
yarn add @ahasend/sdk
```

```bash bun theme={null}
bun add @ahasend/sdk
```

## Try a Sandbox Send

Run this short SDK check locally before adding the longer server integration below. Use a [verified sender](https://ahasend.com/docs/domains.md), a [send-only API key](https://ahasend.com/docs/send-api/credentials.md), and your account ID. It sends to a reserved test address with `sandbox: true`, so no email is delivered.

In the server project directory, add `.env` to `.gitignore`, then create `.env` with `AHASEND_API_KEY`, `AHASEND_ACCOUNT_ID`, and `AHASEND_FROM` (an address on your verified domain). Keep these values out of browser code and AI prompts. Use Node.js 22 or newer. Save this as `quick-send.mjs`:

```js quick-send.mjs theme={null}
import { AhaSendClient } from "@ahasend/sdk";
try {
  const client = AhaSendClient.fromEnv();
  const result = await client.messages.send({
    from: { email: process.env.AHASEND_FROM },
    recipients: [{ email: "recipient@example.com" }],
    subject: "My first sandbox email",
    text_content: "Hello from AhaSend",
    sandbox: true,
  });
  const statuses = result.data.map((entry) => entry.status);
  console.log({ statuses });
  if (!statuses.length || statuses.some((s) => !["queued", "scheduled"].includes(s)))
    throw new Error("A recipient was rejected");
} catch {
  console.error("Sandbox send failed; check your credentials, sender and account.");
  process.exitCode = 1;
}
```

Run `node --env-file=.env quick-send.mjs` from that directory. The output lists each recipient status; `queued` or `scheduled` means accepted for sandbox processing. The process exits with a failure if the request or any recipient fails. Continue below for the full integration.

This gives the agent an unambiguous dependency and lets its normal package install reproduce your dependency graph.

## Step 2: Write the Issue

Give the agent the API contract, security boundaries, and acceptance tests instead of leaving it to infer them. Open an issue, use a prompt like this, and assign it to Copilot:

> **Title:** Add order-confirmation email via AhaSend
>
> **Body:** When an order is created, send a confirmation email using the official AhaSend TypeScript SDK, `@ahasend/sdk`, which is already a dependency. Do not hand-roll HTTP calls or add another email package.
>
> * Validate `AHASEND_API_KEY`, `AHASEND_ACCOUNT_ID`, and the sender address at startup. Construct one `AhaSendClient` at module scope with `apiKey` and `accountId`.
> * Send with `client.messages.send({ from, recipients, subject, html_content, text_content })`. Body fields are snake\_case.
> * Pass a stable, server-derived business identifier as `options.idempotencyKey` so an application-level retry does not create a second email. Reuse a key only for the same exact payload, and include the delivery mode in the key: keys live in one account-wide namespace shared by sandbox and live traffic, and the API matches them against a hash of the request body.
> * Treat the response as multi-status: inspect every entry in `result.data`. A recipient can have `status: "error"` and a null `id` even though the promise resolved.
> * Catch `AhaSendAPIError` from `@ahasend/sdk` and branch on structured fields, not message text. Logs may include only aggregate counts, HTTP status, SDK error code, and request ID. Never log addresses, content, bodies, headers, idempotency keys, or whole request, response, event, client, or error objects.
> * Keep API keys and all AhaSend calls in server-only code. Authenticate and authorize the caller, validate input, and load the order and recipient from server-authoritative storage; do not trust a client-supplied email address or order ID. Bound the request body and rate-limit the triggering route per authenticated caller so a looping or hostile client cannot mail-bomb a customer. Escape any untrusted value before inserting it into HTML.
> * Unit tests must mock the API boundary. Any explicitly approved live integration test must set `sandbox: true` and assert every recipient result.
> * If the task involves webhooks, import `WebhookVerifier` from `@ahasend/sdk/webhooks`, verify the exact raw body, and `await verifier.parse(headers, rawBody)`. Enforce a request-size limit, and commit the verified `webhook-id` and the durable work it guards in one transaction before any side effect, then run that work from a worker outside the request. A bare unique-ID insert loses the event if the process dies after the insert but before processing, and timestamp verification alone does not prevent replays.

Copilot cloud agent also supports repository-wide instructions in `.github/copilot-instructions.md`. Put durable rules such as server-only credentials, stable idempotency keys, multi-status checks, and safe logging there so later email tasks inherit them.

## Step 3: Review the Pull Request

When the agent finishes, it requests your review. For a new task, GitHub limits it to a newly created `copilot/` branch, and Copilot cannot approve or merge the pull request. Review the diff and the agent's session log before allowing workflows or merging.

### Did the key stay server-side?

Scan the entire diff for hardcoded keys and for client-exposed variables such as `NEXT_PUBLIC_` or `VITE_`. Verify that the call is reachable only after your application's normal authentication and authorization checks, that the recipient comes from trusted server-side data, and that the route bounds its request body and rate-limits each authenticated caller. A send path that any caller can reach, or reach without a ceiling, is a mail relay regardless of how correct the SDK usage is. If any of those checks fail, request a correction with an `@copilot` comment on the pull request.

### Is it using the SDK safely?

A representative server-side service looks like this:

```ts lib/send-order-confirmation.ts theme={null}
import { AhaSendAPIError, AhaSendClient } from "@ahasend/sdk";

function requiredEnv(name: string): string {
  const value = process.env[name];
  if (!value) throw new Error(`Missing required environment variable: ${name}`);
  return value;
}

function deliveryMode(): "sandbox" | "live" {
  const value = requiredEnv("AHASEND_DELIVERY_MODE");
  if (value !== "sandbox" && value !== "live") {
    throw new Error("AHASEND_DELIVERY_MODE must be sandbox or live");
  }
  return value;
}

const client = new AhaSendClient({
  apiKey: requiredEnv("AHASEND_API_KEY"),
  accountId: requiredEnv("AHASEND_ACCOUNT_ID"),
});

const mode = deliveryMode();
const sender = requiredEnv("AHASEND_FROM_ADDRESS");

type ConfirmedOrder = Readonly<{
  id: string;
  customerEmail: string;
}>;

export async function sendOrderConfirmation(order: ConfirmedOrder): Promise<void> {
  try {
    const result = await client.messages.send(
      {
        from: { email: sender, name: "Your Store" },
        recipients: [{ email: order.customerEmail }],
        subject: "Your order is confirmed",
        text_content: "Thanks—your order is confirmed.",
        html_content: "<p>Thanks—your order is confirmed.</p>",
        sandbox: mode === "sandbox",
      },
      { idempotencyKey: `order-confirmation-${mode}-${order.id}` },
    );

    const rejected = result.data.filter((entry) => entry.status === "error");
    if (rejected.length > 0) {
      console.warn("AhaSend rejected recipients", { count: rejected.length });
      throw new Error("Order confirmation was not accepted");
    }
  } catch (error) {
    if (error instanceof AhaSendAPIError) {
      console.error("AhaSend request failed", {
        status: error.status,
        code: error.code,
        requestId: error.requestId,
      });
      throw new Error("Email provider request failed");
    }
    throw error;
  }
}
```

The route or controller calling this function still has to authenticate the request, authorize access to the order, and load the order from your database. Keeping the client at module scope also preserves its shared configuration and any enabled local rate pacing across calls.

### Does it handle retries and multi-status results?

The SDK generates an idempotency key for its own retry loop, but a stable caller-provided key is needed when your application may invoke the business operation again. Confirm the PR derives that key from a trusted immutable ID and never logs it.

Keys are scoped to the account, not to a delivery mode, and the API matches a reused key against a hash of the request body. A key built from the order ID alone therefore collides between the sandbox run of Step 4 and the first live send for the same order: the bodies differ only in the `sandbox` field, so the live request is rejected as an idempotency mismatch instead of being sent. Including the mode in the key, as above, keeps the two namespaces apart.

A resolved send returns one entry per recipient. Do not accept code that checks only `result.data[0]`, treats resolution as unconditional success, or exposes recipient details in a response or log.

## Step 4: Test Without Giving the Agent a Production Credential

Prefer mocked tests: Copilot can validate request construction, error handling, multi-status behavior, and idempotency without any AhaSend credential.

If you deliberately want the agent to call the real AhaSend API in sandbox mode, create a dedicated, domain-scoped API key that you can revoke immediately afterward. In the repository, go to **Settings → Secrets and variables → Agents**, store it as `AHASEND_API_KEY`, store the account ID as an Agents secret or variable, and add Agents variables for `AHASEND_FROM_ADDRESS` and `AHASEND_DELIVERY_MODE`. The sender must use the verified domain, and the delivery mode must be `sandbox`. A sandbox send validates the request without delivering email; use `sandbox_result` values such as `deliver`, `bounce`, `defer`, `fail`, and `suppress` to exercise different outcomes.

Create a domain-scoped credential in **Sandbox** mode under **Credentials → Add → API Key v2** for the Agents secret. It simulates sending even when generated code omits `sandbox: true`; see [sandbox credentials](https://ahasend.com/docs/send-api/sandbox.md#how-do-i-enable-sandbox-mode). Keep production credentials out of agent test environments. Keep the agent firewall enabled and explicitly allow `api.ahasend.com` if the request is blocked; do not disable the firewall just to make the test pass.

> [!WARNING]
>
> Never give the agent a production credential. GitHub masks Agents secret values in session logs, but masking is not an authorization boundary and does not prevent a process from using the credential.

Remove the temporary Agents credential after the test. Your deployed application should use a separate key from its hosting platform's secret store.

## Step 5: Merge and Send a Real Email

By default, GitHub Actions workflows do not run automatically when Copilot pushes. First inspect the diff, especially changes under `.github/workflows/`, then use **Approve and run workflows** and confirm the required checks actually completed. Your own approval does not count toward required approvals on a Copilot pull request, so obtain another approval if the repository requires one.

After review and CI pass, merge through your normal protected-branch process. In the deployed environment:

* Set `AHASEND_API_KEY`, `AHASEND_ACCOUNT_ID`, and `AHASEND_FROM_ADDRESS` in the hosting platform's secret store.
* Set `AHASEND_DELIVERY_MODE` explicitly to `live`; fail closed when it is absent or invalid.
* Send one confirmation to an address you control and verify the result and delivery.

## Going Further

For substitutions, attachments and scheduled sends, use the [sending guide](https://ahasend.com/docs/send-api/send-email.md). The [Node.js SDK](https://ahasend.com/docs/guides/nodejs-sdk.md) and [Go SDK](https://ahasend.com/docs/guides/go-sdk.md) references explain client options, per-recipient results and retries. Use [idempotency rules](https://ahasend.com/docs/api-reference/idempotency.md) when your application retries a business operation.

## Troubleshooting

> [accordion] The PR hand-rolls HTTP calls instead of using the SDK
>
> Confirm the dependency and lockfile were committed before the task started. Comment `@copilot replace the raw HTTP calls with the @ahasend/sdk client`, then add the constraint to `.github/copilot-instructions.md`.

> [accordion] The integration test gets an authentication error
>
> Confirm that any deliberately provided credential is stored under **Secrets and variables → Agents**, not under Actions, Codespaces, or Dependabot. Check its scope and revocation state without printing it. Prefer a mocked test if external access is unnecessary.

> [accordion] The sandbox request is blocked
>
> Copilot cloud agent's firewall may block the AhaSend API host. Keep the firewall enabled and add `api.ahasend.com` to its allowlist only if you have accepted the credential risk. Otherwise, use a mocked test.

> [accordion] The generated code throws in a browser
>
> The API client belongs in server-only code. Move the AhaSend client and send call behind an authenticated server route or controller, and ensure no client component imports that module.

Every SDK endpoint is documented in the [API reference](https://ahasend.com/docs/api-reference.md).

## Related Guides

* Before sending: [verify a domain](https://ahasend.com/docs/domains.md) and [create a send-only key](https://ahasend.com/docs/send-api/credentials.md).
* Other ways to send: [REST API](https://ahasend.com/docs/send-api/send-email.md), [SMTP](https://ahasend.com/docs/smtp.md), [CLI quickstart](https://ahasend.com/docs/cli/quickstart.md), [Node.js SDK](https://ahasend.com/docs/guides/nodejs-sdk.md) and [Go SDK](https://ahasend.com/docs/guides/go-sdk.md).
* Request rules: [API authentication](https://ahasend.com/docs/api-reference/authentication.md), [scopes](https://ahasend.com/docs/api-reference/scopes.md), [idempotency](https://ahasend.com/docs/api-reference/idempotency.md), [errors](https://ahasend.com/docs/api-reference/errors.md) and [rate limits](https://ahasend.com/docs/api-reference/rate-limits.md).
* Testing and events: [sandbox mode](https://ahasend.com/docs/send-api/sandbox.md), [CLI webhook testing](https://ahasend.com/docs/cli/webhook-testing.md), [event payloads](https://ahasend.com/docs/api-reference/webhooks.md), [signature verification](https://ahasend.com/docs/api-reference/webhooks/security.md) and [delivery retries](https://ahasend.com/docs/api-reference/webhooks/retry-policy.md).
* Data and limits: [retention](https://ahasend.com/docs/retention.md), [tracking](https://ahasend.com/docs/tracking/open-tracking.md) and [plans and feature availability](https://ahasend.com/docs/facts.md#plans-and-features).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.