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

Once per environment
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.vars

The Worker

src/index.ts
// 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.