# 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:
```bash
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:
```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.

On a Cron Trigger or a Queue consumer, the same function works unchanged: it is one outbound fetch with a bearer token, and Workers place no restriction on that.

---

Base URL: https://emails.sh/v1. Auth: `Authorization: Bearer esh_...`.
Whole API in one file: https://emails.sh/llms.txt. All documentation: https://emails.sh/docs.md.
