Send email from your application #
Prompt:
Email each customer a confirmation when their order ships.
Also works:
- "Send a receipt by email when a payment goes through."
- "Email a team member when someone invites them."
- "Why was our order email refused with email_limit_reached?"
What your tool does #
- Checks the application's plan with
list_applications, and asks you before it moves the application to Pro withset_plan. - Adds the email service to the manifest and resubmits it with
submit_manifest. - Takes the Email package from the library and writes the backend's send with its client, as step 3's sample does, and tests it over the package's test double.
- Reads the month's recipients with
read_usage, and each message's outcome withread_countersandread_logs. - Asks you who receives each message and what it says, where the prompt does not say.
What you need #
- An application on the Pro plan. Email send is a feature of Pro alone, and Plan and usage moves an application to Pro.
- The events that send a message, and who receives each one.
Before your AI starts #
This section is for your AI tool: what it checks and gathers before it begins. You don't need to do these steps yourself.
- A connected, signed-in tool (Connect Claude Code or Codex) and an application with a submitted manifest (Deploy an application).
- The application's identifier and its
plan, fromlist_applications.
Steps #
1. Check the plan #
Email send is a feature of the Pro plan. read_plan_quotas with measure set to email-send-supported returns 1 for Pro and 0 for Free and Standard. On a plan that reads 0, the platform refuses the manifest's email line and every send, email_not_on_plan.
The plan counts the recipients each application's sends address. A Pro application sends to at most 20 recipients an hour, 100 a day, and 1,000 a month, in UTC, and its two environments count together. read_plan_quotas returns these figures as email-recipients-per-hour, email-recipients-per-day, and email-recipients-per-month. The counts keep the service to occasional requests and notices, never newsletters or marketing mail.
2. Declare the email service #
Add {"kind": "email"} to the manifest's services and resubmit it with submit_manifest. The declaration sets up nothing else and needs no setting, no mail provider's key, and no domain of your own.
Every message goes from the platform's own sending address under your application's name. On the production platform, turnzero.ai, the address is no-reply@mail.ai.host, and the person sees the message from "no-reply@mail.dev.ai.host, shown as "
A manifest that declares email stays recorded and deploys after a move off Pro, but every send is refused. Its next submission is refused email_not_on_plan until you remove the line.
3. Send from the backend #
The backend sends with the Email package's client, built once from TURNZERO_CLOUD_API and TURNZERO_CLOUD_TOKEN, the platform credential. The credential names the application and its environment, so a send names neither. Your tool takes the package as Use the library describes.
This module emails a customer when their order ships. A test runs this sample over the Email package's test double and checks the send, the recipients counted, and a refused send.
// src/notify.mjs: emailShipped(customer, order) emails the customer that their order shipped.
import { createEmailClient, EmailRefusal } from '@turnzero/email';
// The platform's origin and this copy's credential, from the settings it receives.
const email = createEmailClient({ api: process.env.TURNZERO_CLOUD_API, token: process.env.TURNZERO_CLOUD_TOKEN });
export async function emailShipped(customer, order) {
try {
const sent = await email.send({
to: [customer.email],
subject: `Order ${order.id} shipped`,
text: `Hello ${customer.name}, your order ${order.id} is on its way and arrives ${order.eta}.`,
replyTo: 'orders@example.com',
});
// messageId names the queued message; recipients counts the addresses accepted.
return { messageId: sent.messageId, recipients: sent.recipients };
} catch (error) {
// A refused send queued nothing: record it, and let the request that called this succeed.
if (!(error instanceof EmailRefusal)) throw error;
console.error(`email refused ${error.refusal}: ${error.detail}`);
return { refused: error.refusal, retryAt: error.resetsAt };
}
}
A message names up to five recipients, in to and cc together, and an address named twice counts once. Its subject is one line of up to 1,000 characters. Its subject, its text, and an optional html body hold 256 kilobytes at most together. A message carries no blind copy and no attachment.
Send once for each event. The client retries nothing, because a send the platform accepted before its answer was lost is already queued, and a second send would email the person twice.
4. Handle a refused send #
A refused send queued nothing. After email_limit_reached, the refusal's resetsAt is the first moment of the next hour, day, or month, in UTC, when the application may send again. Hold the message until then, or send it through your own mail provider.
For more mail than the counts allow, or for mail the service does not send, such as newsletters or attachments, use your own mail provider. Declare its HTTPS API as an upstream with your own key, as Call an external API with an API key describes.
5. Test the sends #
The tests build the client over the package's test double, createEmailDouble from @turnzero/email/testing: pass double.transport as the client's transport, and read what was sent in double.sends. The double answers a send as the platform does, from the plan's figures and a clock the test gives it, and delivers nothing. Test your application locally runs the tests offline.
Expected result #
- The send returns a
messageIdand therecipientsthe platform accepted and counted. The platform answers at once and promises no delivery time. - The platform hands each message to its mail provider within seconds. The application's log stream counts each message under
email_sentoremail_failed, with one line per message naming itsmessage_idand itsrecipients, never an address (Read logs and counters). - A failed line gives its
failure:refusedwhere the mail provider refused the message,exhaustedwhere three attempts failed, ororphanedwhere a delivery was cut off twice. - The mail provider stops mail to an address whose earlier mail bounced, or whose recipient marked a message as spam, and a message to it ends
refused. That holds for every application on the same platform, turnzero.ai or dev.turnzero.ai. read_usagereturns the month's recipients underemail_recipients, with the hour's and the day's counts beside it (Plan and usage). When the month's recipients first pass the warning fraction, the send writes oneusage warningevent into the log stream for each environment.
Refusals #
This table lists the refusals these steps most often meet; the refusals page lists every refusal with its cause and remedy.
| Refusal | Status | Cause | Remedy |
|---|---|---|---|
email_not_on_plan |
409 | A manifest declaring email, or a send, on a plan without email send. The body names the plan and the measure. |
Move the application to Pro with set_plan, or remove the email line and resubmit the manifest. |
email_not_declared |
409 | A send from an application whose manifest declares no email service. | Add {"kind": "email"} to the manifest's services and resubmit it. |
email_limit_reached |
429 | The send would pass the application's count for the hour, the day, or the month. Its period, used, quota, requested, and resets_at members name the count and when it admits again. |
Hold the message until resets_at, or send it through your own mail provider on a declared upstream. |
invalid_request |
400 | A member of the message is malformed, a bcc or a sixth recipient was named, or the message is past its size. The detail names the member or the bound, never an address. |
Correct the named member. |
platform_credential_required |
403 | A send ran under a session or a minted token. | Send from the backend under the platform credential. |
end_user_credential_not_admitted |
403 | A send ran under an end user's session. | Send from the backend under the platform credential. |
Related #
- Plan and usage moves an application to Pro and reads the month's recipients.
- Call an external API with an API key sends mail through your own provider, past the plan's counts.
- Read logs and counters explains how to read the email counters and lines.
- Test your application locally runs the tests on the packages' test doubles.