Cloudflare Workers
A secret binding, one fetch, and the trick that keeps the response fast.
A Worker has fetch and nothing else to install. The key belongs in a secret binding, not in wrangler.jsonc: vars are plain text in your repository and in the dashboard, secrets are not.
Set the key
npx wrangler secret put EMAILSSH_API_KEY
# paste the key from https://emails.sh/dashboard/api-keys when prompted
# For local development, put it in .dev.vars (gitignored):
echo 'EMAILSSH_API_KEY=esh_your_key_here' >> .dev.varsThe Worker
// src/index.ts
export interface Env {
EMAILSSH_API_KEY: string;
}
async function sendEmail(env: Env, to: string) {
const response = await fetch('https://emails.sh/v1/emails', {
method: 'POST',
headers: {
Authorization: `Bearer ${env.EMAILSSH_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
from: 'Acme <hello@acme.com>',
to: [to],
subject: 'Welcome to Acme',
html: '<p>Confirm your address to finish signing up.</p>',
idempotency_key: `signup-${to}`
})
});
if (!response.ok) {
const { error } = (await response.json()) as { error: { code: string; message: string; next?: string } };
throw new Error(`${error.code}: ${error.message} ${error.next ?? ''}`);
}
return (await response.json()) as { id: string };
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
if (request.method !== 'POST') return new Response('Method not allowed', { status: 405 });
const { email } = (await request.json()) as { email: string };
// Answer immediately and let the send finish after the response. waitUntil
// keeps the Worker alive for it; a floating promise without it is killed.
ctx.waitUntil(
sendEmail(env, email).catch((err) => console.error('emails.sh refused the send', err))
);
return Response.json({ accepted: true });
}
} satisfies ExportedHandler<Env>;Use waitUntil only when the caller genuinely does not need the outcome. If the user is waiting to be told the email went, await the send and report the failure.