Frameworks and agents · Published 2026-10-31 · Updated 2026-10-31 · AgentiSend
Send email with Auth0
An Auth0 custom email provider Action that posts the notification to POST /emails and returns code and fix.
To send email with Auth0, deliver the notification from a custom email provider Action that posts JSON to POST /emails. Auth0's configuration guide says to open Auth0 Dashboard, then Branding, then Email Provider, turn on Use my own email provider, choose Custom Provider, and put the From address in the From field. The Action editor on that page is where the code goes. Save deploys it. Send Test Email on that page sends one notification through the Action.
The trigger is custom-email-provider. The function Auth0 calls is onExecuteCustomEmailProvider. The event object documents event.notification.from, to, subject, html, text and message_type. Secrets added with the key icon in the editor arrive on event.secrets. The API object documents api.notification.retry, which asks Auth0 to try the notification again, and api.notification.drop, which asks Auth0 to stop and log the reason. At most one Action can be bound to the trigger. The function below checks the addresses, posts the rendered notification with an idempotency key derived from the message type and the recipient, and on a refusal puts code and fix in that reason. The suite calls the function against a local AgentiSend server.
Auth0's sample retries when the response is a server error and stops otherwise. This Action does that. A refusal such as agent_budget_exceeded is not a server error, so the Action drops it and the reason contains the fix.
Install
No package to add inside the Action. Auth0's sample uses fetch. Paste the function into the editor and add AGENTISEND_API_KEY as a secret. AGENTISEND_BASE_URL is an optional secret when the Action should not call the public API.
Environment
The From address is the From field on the email provider page. Auth0 passes it as event.notification.from. The key is a secret on the Action, not a variable in your app.
| Secret | Required | What it is |
|---|---|---|
AGENTISEND_API_KEY | yes | A key with sending_access. |
AGENTISEND_BASE_URL | no | Defaults to https://api.agentisend.com. |
The route
/**
* Auth0 — the custom email provider Action.
*
* Auth0 calls `onExecuteCustomEmailProvider` on the `custom-email-provider`
* trigger. `event.notification` carries from, to, subject, html, text and
* message_type. `event.secrets` holds the values you add in the Action
* editor. `api.notification.retry` asks Auth0 to try again;
* `api.notification.drop` asks it to stop. This file posts JSON to
* POST /emails with fetch, which is the call Auth0's own sample makes.
*
* A server error asks for a retry, matching Auth0's sample. Any other
* refusal asks Auth0 to drop the notification. The suite passes secrets on
* the event and falls back to the process environment.
*/
export interface Auth0Notification {
from?: string;
to?: string;
subject?: string;
html?: string;
text?: string;
message_type?: string;
}
export interface Auth0EmailEvent {
notification?: Auth0Notification;
secrets?: Record<string, string | undefined>;
}
export interface Auth0EmailApi {
notification: {
drop: (reason: string) => void;
retry: (reason: string) => void;
};
}
/** Enough to refuse an obvious typo here; the API checks the address properly. */
const ADDRESS = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
function secret(event: Auth0EmailEvent, name: string): string {
const fromEvent = event.secrets?.[name];
if (fromEvent) return fromEvent;
const fromProcess = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env?.[
name
];
return fromProcess ?? '';
}
function baseUrl(event: Auth0EmailEvent): string {
return (secret(event, 'AGENTISEND_BASE_URL') || 'https://api.agentisend.com').replace(/\/+$/, '');
}
export async function onExecuteCustomEmailProvider(event: Auth0EmailEvent, api: Auth0EmailApi): Promise<void> {
const note = event.notification ?? {};
const to = String(note.to ?? '').trim();
const from = String(note.from ?? '').trim();
const subject = String(note.subject ?? '');
const html = typeof note.html === 'string' && note.html.length > 0 ? note.html : undefined;
const text = typeof note.text === 'string' && note.text.length > 0 ? note.text : undefined;
if (!ADDRESS.test(to) || !ADDRESS.test(from) || (!html && !text)) {
api.notification.drop('email must be one address, like you@example.com');
return;
}
const messageType = note.message_type && note.message_type.length > 0 ? note.message_type : 'notification';
const payload: Record<string, string> = { from, to, subject };
if (html) payload.html = html;
if (text) payload.text = text;
try {
const response = await fetch(`${baseUrl(event)}/emails`, {
method: 'POST',
headers: {
authorization: `Bearer ${secret(event, 'AGENTISEND_API_KEY')}`,
'content-type': 'application/json',
accept: 'application/json',
// The message type and the recipient: a retry of the same notification replays.
'idempotency-key': `auth0/${messageType}/${to}`,
},
body: JSON.stringify(payload),
});
if (response.ok) return;
const parsed = (await response.json()) as { error?: { code?: string; fix?: string } };
const reason = `${parsed.error?.code ?? 'refused'}: ${parsed.error?.fix ?? ''}`.slice(0, 1024);
// Auth0's sample retries a server error and stops on anything else.
if (response.status >= 500) api.notification.retry(reason);
else api.notification.drop(reason);
} catch (err) {
const message = err instanceof Error ? err.message : 'send failed';
api.notification.drop(message.slice(0, 1024));
}
}Try it
In the email provider editor, click Send Test Email. The message id is in your AgentiSend log. GET /emails/{id} is that notification.
What comes back when it goes wrong
Every refused request carries code, message, fix and docs_url. The Action puts code and fix in the reason passed to api.notification.drop or api.notification.retry, and Auth0 writes that reason to the log for the notification. An address with no domain is dropped before POST /emails. The error catalogue lists every code.