Frameworks and agents · Published 2026-10-31 · Updated 2026-10-31 · AgentiSend
Send email with Encore (TypeScript)
An Encore.ts endpoint that sends one workspace notice with the Node SDK and an idempotency key.
To send email with Encore for TypeScript, wrap one async function in api() from encore.dev/api. The defining-APIs guide says api({ expose: true, method: 'POST', path: '/send' }, fn) is a public endpoint, and that expose defaults to private. The function below checks the address, calls emails.send with an idempotency key derived from the recipient, and returns the message id or the refusal's code and fix. The suite calls the function against a local AgentiSend server. It does not load encore.dev.
The id in the response is the record. Read it with GET /emails/{id}.
Install
pnpm add agentisendDeclare the endpoint next to the function. Encore.ts reads the options object at compile time:
import { api } from 'encore.dev/api';
import { send } from './send';
export const sendEmail = api(
{ expose: true, method: 'POST', path: '/send' },
async (params: { email?: string; note?: string }) => send(params),
);Secrets use secret() from encore.dev/config. The secrets guide says you set the value with encore secret set --type dev,local <Name>, or in the dashboard under Settings, then Secrets, and you read it by calling the function secret() returned. Pass that value into the client if you do not want the process environment to hold it.
Environment
| Variable | Required | What it is |
|---|---|---|
AGENTISEND_API_KEY | yes | A key with sending_access. In Encore this is the secret you set, not a value committed in source. |
MAIL_FROM | yes | The From address, on a domain you have verified. |
AGENTISEND_BASE_URL | no | Defaults to https://api.agentisend.com. |
The route
/**
* Encore for TypeScript — the function `api()` wraps.
*
* The documented endpoint is:
*
* import { api } from 'encore.dev/api'
* export const send = api(
* { expose: true, method: 'POST', path: '/send' },
* async (params: Params): Promise<Reply> => send(params),
* )
*
* There is no Encore package for this API. The function posts through the
* Node SDK. The suite calls `send` on Node and does not load `encore.dev`.
*
* AGENTISEND_API_KEY, MAIL_FROM, and optionally AGENTISEND_BASE_URL.
* Encore's own secret helper is `secret()` from `encore.dev/config`; this
* file reads the variables the suite sets, which is what that helper returns
* once you have set them.
*/
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@]+$/;
export async function send(params: {
email?: string;
note?: string;
}): Promise<{ status: number; body: Record<string, unknown> }> {
const email = String(params.email ?? '').trim();
const note = String(params.note ?? '');
if (!ADDRESS.test(email)) {
return { status: 400, body: { error: 'email must be one address, like you@example.com' } };
}
try {
const { id } = await agentisend.emails.send(
{
from: mailFrom(),
to: email,
subject: 'Your workspace is ready',
text: `Workspace note: ${note}`,
},
{ idempotencyKey: `encore/${email}` },
);
return { status: 200, body: { id } };
} catch (err) {
if (err instanceof AgentiSendError) {
return { status: err.status, body: { code: err.code, fix: err.fix } };
}
return { status: 500, body: { error: 'send failed' } };
}
}Try it
curl -X POST http://127.0.0.1:4000/send \
-H 'content-type: application/json' \
-d '{"email":"you@example.com","note":"workspace"}'body.id on the function's return value is the message id.
What comes back when it goes wrong
Every refused request carries code, message, fix and docs_url. The function returns code and fix with the API's status so the endpoint can render them. An address with no domain never reaches POST /emails. The error catalogue lists every code.