# Waahy email API

A standalone email service hosted on Cloudflare Workers. Sends as the owner's
connected Gmail account. Call it from trusted servers, scripts, or agents.

## Send an email

POST https://hobbymail.marindrano.com/v1/email/send

Authentication: Authorization: Bearer $EMAIL_SERVICE_KEY
Content-Type: application/json

```bash
curl --fail-with-body 'https://hobbymail.marindrano.com/v1/email/send' \
  -H "Authorization: Bearer $EMAIL_SERVICE_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"to":"reader@example.com","subject":"Hello","text":"Hello from my app!"}'
```

| Field | Required | Limit |
| --- | --- | --- |
| to | yes | One email address, up to 320 characters |
| subject | yes | 1 to 200 characters, no line breaks |
| text | yes | 1 to 20,000 characters |
| html | no | 1 to 100,000 characters, sent alongside text |

Maximum JSON request size: 512 KiB. UTF-8 subjects and bodies are supported.
The sender is fixed by the Gmail account. Attachments, bulk recipients, and
custom sender addresses are not supported.

## Responses

- 200: {"sent":true}, Gmail accepted the message. This is not an inbox-delivery guarantee.
- 400: Invalid JSON or fields.
- 401: Missing or invalid bearer token.
- 413: Request body too large.
- 429: Shared send budget spent. Wait for the Retry-After header in seconds.
- 500: Service unconfigured, quota storage unavailable, or Gmail rejected the send.

Errors use {"error":{"code":"invalid_request","message":"...","requestId":"..."}}.
Every response has an X-Request-Id header. Keep it when reporting a failure.

## Shared budget and retries

30 valid send attempts per UTC calendar minute, 400 per UTC calendar day,
shared by all callers and locations. Attempts that reach Gmail count even if
Gmail refuses. Invalid and unauthorized requests do not consume the budget.
Other apps using the same Gmail account share Google's own sending limits.

Sending is synchronous. There is no queue or idempotency key. If a request
times out, delivery can be uncertain. Retrying can send a duplicate.

## Credentials

The caller needs only EMAIL_SERVICE_KEY. Keep it server-side. Never put it in
a public browser bundle, mobile app, URL, or shared documentation. Gmail OAuth
credentials stay inside the Worker. Rotating the service key changes it for
all callers. This public document contains no secrets.

GET https://hobbymail.marindrano.com/health returns {"ok":true}. This is a liveness check, not a Gmail check.
GET https://hobbymail.marindrano.com/docs returns this Markdown document.
