Frameworks and agents · Published 2026-10-31 · Updated 2026-10-31 · AgentiSend
Send email with AWS Lambda
An AWS Lambda handler that sends with the Node SDK and returns the message id from an API Gateway event.
To send email with AWS Lambda, export handler and point the function at it. API Gateway's proxy integration delivers the JSON body as a string on event.body and expects { statusCode, body } back, which is the shape this file returns. The Node SDK runs in the function's Node runtime. The idempotency key is the recipient, so a retried invocation replays instead of sending a second report. The suite runs this file against a local AgentiSend server.
A function another system can invoke in a loop is where the account kill switch earns its place. POST /limits/kill-all pauses every key; while it is on, POST /emails answers kill_switch_active and queued messages stay queued. One report's record is GET /emails/{id}.
Install
pnpm add agentisendShip node_modules with the function, or bundle the import into the zip the runtime loads.
Environment
Set the variables on the function configuration. A value in your shell is not on the function.
| Variable | Required | What it is |
|---|---|---|
AGENTISEND_API_KEY | yes | A key with sending_access. |
MAIL_FROM | yes | The From address, on a domain you have verified. |
AGENTISEND_BASE_URL | no | Defaults to https://api.agentisend.com. |
Give the function a timeout above the SDK's 30 second request limit so a slow answer is the API's timeout, not the runtime killing the process mid-request.
The route
/**
* AWS Lambda — the handler behind an API Gateway proxy event.
*
* The runtime calls `handler` with the event. The JSON body is a string.
* AGENTISEND_API_KEY and MAIL_FROM are function environment variables.
* AGENTISEND_BASE_URL is optional and defaults to the public API.
*/
import { AgentiSend, AgentiSendError } from 'agentisend';
const agentisend = new AgentiSend();
function mailFrom(): string {
const from = process.env.MAIL_FROM;
if (!from) throw new Error('Set MAIL_FROM to an address on a domain you have verified.');
return from;
}
/** Enough to refuse an obvious typo here; the API checks the address properly. */
const ADDRESS = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
interface ProxyEvent {
body?: string | null;
}
interface ProxyResult {
statusCode: number;
headers: { 'content-type': string };
body: string;
}
function reply(statusCode: number, payload: unknown): ProxyResult {
return {
statusCode,
headers: { 'content-type': 'application/json' },
body: JSON.stringify(payload),
};
}
export async function handler(event: ProxyEvent): Promise<ProxyResult> {
let email = '';
let note = '';
try {
const parsed = JSON.parse(event.body ?? '') as { email?: string; note?: string };
email = String(parsed.email ?? '').trim();
note = String(parsed.note ?? '');
} catch {
return reply(400, { error: 'email must be one address, like you@example.com' });
}
if (!ADDRESS.test(email)) {
return reply(400, { error: 'email must be one address, like you@example.com' });
}
try {
const { id } = await agentisend.emails.send(
{
from: mailFrom(),
to: email,
subject: 'Your report is ready',
text: `Report note: ${note}`,
},
// The recipient, not the clock: a retried invocation replays this send.
{ idempotencyKey: `report/${email}` },
);
return reply(200, { id });
} catch (err) {
if (err instanceof AgentiSendError) {
return reply(err.status, { code: err.code, fix: err.fix });
}
return reply(500, { error: 'send failed' });
}
}The handler name in the function configuration is handler.handler when this file is handler.js.
Try it
Invoke with a proxy event whose body is the JSON string {"email":"you@example.com","note":"q3"}. The result's body is {"id":"…"}.
What comes back when it goes wrong
Every refused request carries code, message, fix and docs_url. The handler puts code and fix in the proxy body and copies the API's status into statusCode. The error catalogue lists every code. A missing or non-JSON body is a 400 from this file, and POST /emails is not called.