# Send email

```yaml
capability_id: email.send
display_name: Send email
ui_capability_key: send_email
scope: email.send
endpoints:
  - method: POST
    path: /send/ses
  - method: POST
    path: /send/postmark
  - method: POST
    path: /send/resend
operation: email.send
billable: true
environment: required
```

## Purpose and authorization

`email.send` provides provider-native single-message email sending through
AWS SES, Postmark, or Resend. The application selects the provider by choosing
the corresponding endpoint. Gateman does not select a provider, fail over
between providers, expose a generic `/send/email` route, or provide batch
sending.

Requests require authenticated [Contract v1](https://docs.getgateman.com/contract-v1/)
transport and the application scope `email.send`. Gateman applies the
configured provider credential at the production provider boundary; the
application does not send provider authentication in the request.

Production sends use the selected provider. Staging and development use
Gateman's local mailbox capture and do not contact an external provider.

## Automatic sending limits

An account owner may configure an optional application-wide production safety
limit in Sending Protection. The limit counts normalized recipient attempts
across SES, Postmark, and Resend using a daily, weekly, or monthly UTC calendar
period. Nonproduction mailbox captures are excluded.

When the final admitted operation reaches the threshold, it is allowed to
execute and Gateman suspends further email sending. A request that would exceed
the remaining allowance is rejected as a whole; no recipients from that request
are counted. A new period does not automatically resume a suspended
application. An authorised resume resets the counter to zero while retaining
the configured limit and period.

Production email requests can return these `403` errors:

- `scope_not_allowed` — `email.send` is not authorised for the application.
- `email_sending_suspended` — sending is suspended through Sending Protection.
- `email_sending_limit_exceeded` — this request triggered the configured limit.

Do not treat these errors as provider outages or retry them immediately. Pause
application workers or queues where appropriate, preserve unsent messages, and
resume deliberately after authorised recovery. Operations already admitted
before suspension may still complete; transport-uncertain operations must not
be blindly replayed.

## Plan entitlement

Standard and Premium applications can send through SES, Postmark, and Resend,
and both plans can use nonproduction mailbox capture. Standard sending does not
use Gateman-managed hard-bounce or application complaint suppression, managed
provider tracking, or provider-event ingestion. Premium adds Gateman's global
permanent-hard-bounce suppression, application-scoped complaint suppression,
managed provider tracking, and verified provider-event ingestion.

Gateman's suppression layer is an additional protection layer. It does not
replace provider-native reputation, bounce, complaint, consent, unsubscribe,
or sender-policy controls, including controls applied independently by SES,
Postmark, or Resend.

## Implementation contract

### Authentication and common request rules

All three routes are authenticated POST requests. The exact signature
construction, timestamp window, raw-body rules, and error envelope are defined
by Contract v1.

```http
POST /send/postmark
Content-Type: application/json
X-GM-App: app_...
X-GM-Ts: 1710000000
X-GM-Sig: <Contract-v1-signature>
X-GM-Idempotency-Key: order-email-123
```

`X-GM-Idempotency-Key` is optional. If supplied, it is bound into the
Contract v1 signature and follows the email idempotency rules below.

Every request body contains a required `environment` with one of
`production`, `staging`, or `development`. The official PHP, Node.js, Python,
and Go clients inject the configured environment for environment-aware routes;
direct HTTP integrations must include it themselves.

Provider credentials, provider authentication headers, and AWS credentials
are managed by Gateman. The application must not put them in these payloads.

The maximum supported HTTP request-body size for `email.send` is 4 MiB
(4,194,304 bytes), measured across the complete JSON payload, including any
Base64-encoded attachments. This overall limit takes precedence over the
individual message-body and attachment limits; those limits continue to apply
independently. Requests exceeding 4 MiB are outside the supported API
contract.

### SES: `POST /send/ses`

```json
{
  "environment": "production",
  "to": ["recipient@example.test"],
  "cc": [],
  "bcc": [],
  "subject": "Your report is ready",
  "text": "Your report is ready.",
  "html": "<p>Your report is ready.</p>",
  "reply_to": "reply@example.test",
  "attachments": [
    {
      "filename": "report.txt",
      "content_type": "text/plain",
      "content_base64": "SGVsbG8K"
    }
  ]
}
```

`to` must be a non-empty array of valid email addresses. `cc` and `bcc` are
optional arrays of valid addresses. There may be at most 10 combined
recipients. `subject` is required and must not contain CR or LF. At least one
of `text` or `html` is required. Each body is limited to 4 MiB; the combined
body limit is 8 MiB.

`attachments` is optional and must be an array of at most five entries. Each
entry uses `filename`, `content_type`, and strict base64 `content_base64`.
Filenames must match the provider-safe filename format, each decoded
attachment is limited to 5 MiB, and decoded attachments together are limited
to 8 MiB. When attachments are present, the SES MIME message generated by
Gateman is limited to 9 MiB. Applications do not submit raw MIME messages
directly.

SES controls sender identity from Gateman configuration. A caller-supplied
`from` field is rejected with `sender_identity_managed_by_gateman`; it is not
silently used. The configured sender email is required. The configured sender
display name is used when it is non-empty and at most 64 characters; otherwise
SES uses the configured sender email alone. CR/LF in the configured display
name is normalized to spaces.

`reply_to` is optional. A valid plain email address is used. An omitted, empty,
or invalid non-CR/LF value falls back to the configured SES Reply-To address;
if that configured value is absent or invalid, Reply-To is omitted. A
CR/LF-containing value is rejected with an invalid `reply_to` error. Display
name forms are not valid Reply-To values and follow the invalid-value fallback
rule.

Unknown SES fields are not part of the contract and are ignored. In
particular, SES does not accept caller-defined provider headers through this
route. SES-specific configuration such as the sender identity and optional
configuration set remains application configuration.

### Postmark: `POST /send/postmark`

Postmark preserves its native single-message field names:

```json
{
  "environment": "production",
  "From": "Acme Reports <sender@example.test>",
  "To": ["recipient@example.test"],
  "Cc": [],
  "Bcc": [],
  "Subject": "Your report is ready",
  "TextBody": "Your report is ready.",
  "HtmlBody": "<p>Your report is ready.</p>",
  "ReplyTo": "reply@example.test",
  "Headers": [
    {"Name": "X-Campaign", "Value": "spring"}
  ],
  "Attachments": [
    {
      "Name": "report.txt",
      "Content": "SGVsbG8K",
      "ContentType": "text/plain"
    }
  ],
  "MessageStream": "outbound"
}
```

The route also accepts the lowercase aliases used by the active adapter
(`from`, `to`, `cc`, `bcc`, `subject`, `text`, `html`, `reply_to`, `headers`,
`attachments`, and `message_stream`). `To`, `Cc`, and `Bcc` may be arrays or
comma-separated strings. `Headers` is an array of `{Name, Value}` objects.
`Attachments` uses `Name`, `Content`, and `ContentType`; content is strict
base64. `MessageStream` is an optional caller-selectable Postmark field. A
valid string is forwarded unchanged as native `MessageStream`; when omitted,
Gateman does not insert a default and Postmark applies its normal default
behavior. Empty, whitespace-padded, non-string, or control-character values
are rejected with `Invalid MessageStream`. The field is Postmark-specific and
is not a universal SES or Resend option. Staging and development capture
locally without invoking Postmark.

The sender may be a plain email or a valid display-name form such as
`Acme Reports <sender@example.test>`. `ReplyTo` must be a valid plain email;
there is no SES-style configured fallback. CR/LF in header values is rejected.
At most 50 combined recipients are accepted. Each body is limited to 4 MiB.
There may be at most five attachments, each decoded attachment is limited to
5 MiB, and decoded attachments together are limited to 8 MiB.

Provider authentication fields are managed by Gateman. Fields whose names
look like authorization, API-key, or token inputs are rejected with
`provider_authentication_is_managed_by_gateman`. Other unknown fields are not
part of the contract and are ignored.

### Resend: `POST /send/resend`

Resend preserves its native lowercase field names:

```json
{
  "environment": "production",
  "from": "Acme Reports <sender@example.test>",
  "to": ["recipient@example.test"],
  "cc": [],
  "bcc": [],
  "subject": "Your report is ready",
  "text": "Your report is ready.",
  "html": "<p>Your report is ready.</p>",
  "reply_to": "reply@example.test",
  "headers": {
    "X-Campaign": "spring"
  },
  "attachments": [
    {
      "filename": "report.txt",
      "content": "SGVsbG8K",
      "content_type": "text/plain"
    }
  ]
}
```

`to`, `cc`, and `bcc` may be arrays or comma-separated strings. `headers` is
an object of header names to string values. Attachments use `filename`,
base64 `content`, and optional `content_type` (defaulting to
`application/octet-stream`). Remote-path attachments are not accepted.

The sender may be a plain email or a valid display-name form. `reply_to` must
be a valid plain email and has no SES-style fallback. Header names and values
are validated; CR/LF and provider-authentication headers are rejected. The
recipient, body, and attachment limits are the same as Postmark: 50 combined
recipients, 4 MiB per body, five attachments, 5 MiB per decoded attachment,
and 8 MiB decoded total. Unknown fields are ignored unless they are rejected
as provider-authentication inputs.

## Environment behavior

| Environment | SES, Postmark, and Resend behavior |
| --- | --- |
| `production` | Sends through the explicitly selected provider. |
| `staging` | Validates and captures in Gateman's local mailbox; no provider call. |
| `development` | Validates and captures in Gateman's local mailbox; no provider call. |

Captured messages, including content and attachments, are available through
Gateman's authorized mailbox facilities for seven days. Capture does not
initiate external provider delivery. Postmark and Resend nonproduction
capture does not require their production provider tokens. SES capture still
requires the configured SES sender identity and applicable local validation.
One accepted capture is one billable `email.send` operation.

## Provider readiness

`POST /email/readiness` is an independent, non-billable advisory endpoint. It
requires `email.send` and accepts only `provider` and `environment`:

```http
POST /email/readiness
Content-Type: application/json
```

```json
{
  "provider": "ses",
  "environment": "production"
}
```

`provider` must be `ses`, `postmark`, or `resend`. `environment` must be a
valid application environment; it is validated but does not change the
readiness evaluation. A successful response is:

```json
{
  "ok": true,
  "provider": "ses",
  "ready": true,
  "missing": [],
  "invalid": []
}
```

Readiness checks relevant configuration presence and limited syntax. For SES
that includes the configured region and sender email; for Postmark and Resend
it includes the configured provider token. Readiness does not test provider
availability, credential permissions or validity at the provider,
sender/domain verification, quota, acceptance, or delivery.

## Successful responses and delivery meaning

SES success does not include a `provider` field:

```json
{
  "ok": true,
  "caller": "app_example",
  "message_id": "ses-message-id-example",
  "error": null
}
```

Postmark and Resend success include the selected provider:

```json
{
  "ok": true,
  "caller": "app_example",
  "provider": "postmark",
  "message_id": "postmark-message-id-example",
  "error": null
}
```

For a production request, `message_id` identifies the message accepted by the
selected provider: SES `MessageId`, Postmark `MessageID`, or Resend `id`. For
a nonproduction request, it identifies Gateman's local capture. A successful
response does not mean delivery to a receiving server, delivery to a mailbox,
receipt by a person, or that a person read the message.

Later delivery reporting is provider-specific. SES event reporting remains a
separate SES capability; this capability does not imply equivalent Postmark
or Resend event ingestion.

## Suppression and sending safety

Premium Gateman applications check every address in `to`, `cc`, and `bcc`
before provider execution or local capture. Permanent hard-bounce suppression
can apply globally; complaint suppression can apply to the application. A
matching suppression rejects the entire request with HTTP `400` and
`Recipient suppressed`.

Standard applications do not use Gateman's suppression list for sending. They
remain subject to any independent controls enforced by the selected provider.

If the Premium suppression safety check is unavailable, the entire request
returns HTTP `503` with `Mail safety check unavailable`. Neither outcome sends
to a provider or creates a nonproduction capture, and neither consumes a
billable operation. After a temporary safety-check failure, the same
idempotency key and unchanged payload may be attempted again.

Gateman's suppression layer does not replace the application's responsibility
for consent, lawful sending, unsubscribe handling, or message policy.

## Idempotency and retry obligations

`X-GM-Idempotency-Key` is optional. Leading and trailing ASCII spaces and tabs
are trimmed. An absent, empty, or whitespace-only key is treated as omitted;
an omitted key does not activate email idempotency protection. A non-empty key
over 255 bytes or containing prohibited ASCII control characters is rejected.
A valid non-empty key is included in the Contract v1 signature. The key is
scoped to the application and selected provider; the environment and
provider-native payload participate in the request fingerprint.

The same application, provider, key, and payload produce a retained replay.
An exact successful replay returns HTTP `200` and does not consume another
operation or send another message. A retained definite failure returns HTTP
`409` without another send. Reusing the key with a changed payload or
environment returns HTTP `409` with `idempotency_key_payload_conflict`.

Terminal successful and definite-failure results are retained for 15 minutes.
An active or unresolved request returns HTTP `409` with
`mail_send_indeterminate`. Provider-started or otherwise uncertain outcomes
remain protected beyond the ordinary terminal window; a new key does not
make an uncertain production send safe to duplicate.

If the key is omitted, Gateman does not provide duplicate suppression for the
request. A confirmed pre-admission rejection can be retried when its response
semantics permit it. A timeout or other ambiguous outcome after processing may
mean that a provider accepted the message. Do not assume that a fresh key,
expired terminal retention, or an HTTP timeout makes repeating an uncertain
send safe. Official clients do not automatically retry.

## Accounting and operational limits

One accepted send or capture is one billable `email.send` operation, regardless
of recipient count. Provider rejection, missing provider credentials discovered
after admission, and later send failure can remain billable. Authentication,
request validation, suppression rejection, pre-admission provider concurrency
rejection, and monthly-limit rejection do not consume an operation. Matching
idempotent replays do not consume another operation.

Application admission can reject an authenticated request with HTTP `429` and
`app_concurrency_limit_exceeded` plus `Retry-After`, or HTTP `503` with
`app_concurrency_guard_unavailable`. In production, provider-bound admission
can return HTTP `429` with `Too many concurrent provider requests` and
`Retry-After: 1`, or HTTP `503` with `Provider concurrency guard unavailable`.
Nonproduction capture does not use provider concurrency admission.

Monthly allowance exhaustion returns HTTP `403` with
`monthly_operation_limit_reached` and allowance details. If operation
accounting is unavailable, the route returns HTTP `503` with
`Operation accounting unavailable`. Provider rate limiting returns HTTP `429`
with `provider_rate_limited`; a valid provider `Retry-After` may be forwarded.

## Error outcomes

| Condition | HTTP response |
| --- | --- |
| Authentication, signature, stale request, or missing scope | HTTP `401` with the Contract v1 authentication error, including `Scope not allowed` where applicable. |
| Invalid JSON, environment, recipients, body, header, sender, or attachment | HTTP `400` with the provider-specific validation error. |
| Caller-supplied SES `from` | HTTP `400`, `sender_identity_managed_by_gateman`. |
| Caller-supplied provider authentication field | HTTP `400`, `provider_authentication_is_managed_by_gateman`. |
| Confirmed suppressed recipient | HTTP `400`, `Recipient suppressed`. |
| Suppression safety unavailable | HTTP `503`, `Mail safety check unavailable`. |
| Application concurrency limit/unavailability | HTTP `429`/`503`, `app_concurrency_limit_exceeded` or `app_concurrency_guard_unavailable`. |
| Production provider concurrency limit/unavailability | HTTP `429`/`503`, `Too many concurrent provider requests` or `Provider concurrency guard unavailable`. |
| Monthly allowance exhausted | HTTP `403`, `monthly_operation_limit_reached`. |
| Operation accounting unavailable | HTTP `503`, `Operation accounting unavailable`. |
| Provider credential absent after admission | HTTP `400`, `SES not configured` or `Provider not configured`, depending on provider. |
| Provider rejects a request | HTTP `400`, `provider_rejected` for Postmark/Resend; SES returns its send-failure response. |
| Provider rate limit | HTTP `429`, `provider_rate_limited`, with provider details and possible `Retry-After`. |
| Provider or transport outcome is uncertain | HTTP `409`, `mail_send_indeterminate`, `message_id: null`. |
| Local send/capture failure | Provider route-specific `400`/`503` failure response; do not interpret it as delivery. |
| Exact successful replay | HTTP `200` with the retained successful response; no additional send or operation. |
| Retained definite-failure replay | HTTP `409` with the retained failure response. |
| Active or indeterminate request | HTTP `409`, `mail_send_indeterminate`. |
| Changed payload or environment under the same key | HTTP `409`, `idempotency_key_payload_conflict`. |
| Readiness dependency failure | HTTP `503`, `Email readiness unavailable`. |

Provider rejection details are provider-specific. Gateman does not promise a
common provider error-code vocabulary or a common provider delivery status.

## Data retention and privacy

Nonproduction captured messages, including their content and attachments, are
available for seven days. Production email retains operational metadata for
31 days; derived daily activity summaries are retained for 60 days. It is not
a permanent message archive. Terminal email idempotency
results are retained for 15 minutes, subject to separate protection for
unresolved outcomes. Suppression remains enforced until appropriately cleared.
Provider-side retention and delivery reporting are independent of Gateman.

Applications SHOULD avoid unnecessarily logging message content, recipient
lists, provider responses, or authentication material. Gateman does not make
WordPress-local email logging part of this API capability; a WordPress
integration's local log policy remains its own responsibility.

## Implementation outcome

A conforming integration should produce these observable outcomes:

- It authenticates with Contract v1 and selects the intended provider endpoint.
- It constructs the selected provider's request shape without unapproved
  field translation.
- It sends with the intended application environment.
- It distinguishes production provider acceptance from nonproduction capture.
- It applies the correct sender and Reply-To rules for the selected provider.
- It uses optional idempotency appropriately and understands replay outcomes.
- It recognizes suppressed recipients and temporary safety-check unavailability.
- It accounts for accepted billable sends and captures.
- It handles provider failures and uncertain outcomes without unsafe retries.
- It does not confuse provider acceptance with recipient delivery.
- It protects message content and recipient information.

## Implementation instructions for agents

- **MUST** authenticate every send and readiness request with Contract v1 and
  the `email.send` scope.
- **MUST** select exactly one provider endpoint: `/send/ses`,
  `/send/postmark`, or `/send/resend`; **MUST NOT** invent `/send/email` or
  automatic provider failover.
- **MUST** send the selected provider's native payload shape and include a
  valid `environment`.
- **MUST** keep provider authentication out of request payloads and headers.
- **MUST** apply SES sender and Reply-To rules as documented; **MUST NOT** use
  a caller-supplied SES `from` value.
- **MUST** treat production acceptance and nonproduction capture as different
  outcomes; a `message_id` is not proof of recipient delivery.
- **MUST** handle `Recipient suppressed`, `Mail safety check unavailable`,
  provider rejection, rate limiting, and indeterminate outcomes explicitly.
- **SHOULD** use a stable idempotency key for each logical production email
  where appropriate. After an ambiguous result, the original key and
  identical payload preserve duplicate protection while the original request
  remains protected; a same-key replay may continue to return HTTP `409`,
  `mail_send_indeterminate`, and does not establish provider acceptance or
  rejection.
- **MUST NOT** automatically generate a new key to bypass an unresolved
  outcome. **SHOULD** use an appropriate reconciliation process rather than
  repeatedly polling the send endpoint. Expiration of the ordinary 15-minute
  terminal replay window does not make an uncertain message safe to resend.
- Official Gateman clients do not automatically retry.
- **MUST** allow for an additional billable email.send operation when deliberately
  making a new send attempt.
- **SHOULD** keep message content, recipient information, credentials, and
  provider responses out of unnecessary persistent logs.

## Reusable AGENTS.md block

```markdown
<!-- gateman:begin capability=email.send -->
- Use authenticated Contract v1 with the `email.send` scope and select exactly
  one provider endpoint: `/send/ses`, `/send/postmark`, or `/send/resend`.
- Preserve the selected provider's native payload shape and include
  `environment`: production sends; staging/development capture locally.
- Keep provider credentials and authentication headers out of request payloads.
- Apply provider-specific sender, Reply-To, recipient, body, header, and
  attachment rules; SES does not accept caller-supplied `from`.
- Treat capture as non-delivery and provider `message_id` as acceptance or
  capture correlation, never as recipient delivery.
- Use a stable `X-GM-Idempotency-Key` for logical production sends where
  appropriate. After an ambiguous result, the original key and identical
  payload preserve duplicate protection; a same-key replay may still return
  HTTP `409`, `mail_send_indeterminate`, and does not resolve provider
  acceptance uncertainty.
- Do not automatically generate a new key to bypass an unresolved outcome.
  Use appropriate reconciliation rather than repeatedly polling the send
  endpoint; expiry of the ordinary 15-minute terminal replay window does not
  make an uncertain message safe to resend. Official clients do not
  automatically retry.
- Terminal replay protection lasts 15 minutes; unresolved provider-started
  outcomes remain protected longer. Never assume expiry makes an uncertain
  send safe to repeat.
- Handle suppression, safety-check unavailability, provider rejection,
  rate-limiting, and `mail_send_indeterminate` explicitly.
- Remember that accepted sends/captures consume `email.send` operations;
  idempotent replays do not.
- Keep message content, recipient information, credentials, and provider
  responses out of unnecessary persistent logs.
- Full reference: https://docs.getgateman.com/capabilities/email.send/
<!-- gateman:end capability=email.send -->
```

## Related references

- [Contract v1](https://docs.getgateman.com/contract-v1/)
- [Authentication](https://docs.getgateman.com/authentication/)
- [Environments](https://docs.getgateman.com/environments/)
- [Idempotency](https://docs.getgateman.com/idempotency/)
- [Operation Accounting](https://docs.getgateman.com/operation-accounting/)
- [Errors and responses](https://docs.getgateman.com/errors-and-responses/)
- [Rate limits and concurrency](https://docs.getgateman.com/rate-limits-and-concurrency/)
- [Data retention](https://docs.getgateman.com/data-retention/)
- [Official PHP client](https://docs.getgateman.com/canonical-client-php/)
- [Official Node.js client](https://docs.getgateman.com/canonical-client-node/)
- [Official Python client](https://docs.getgateman.com/canonical-client-python/)
- [Official Go client](https://docs.getgateman.com/canonical-client-go/)
