Gateman
Documentation/Send email

Send email

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 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.

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

{
  "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:

{
  "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:

{
  "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

EnvironmentSES, Postmark, and Resend behavior
productionSends through the explicitly selected provider.
stagingValidates and captures in Gateman's local mailbox; no provider call.
developmentValidates 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:

POST /email/readiness
Content-Type: application/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:

{
  "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:

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

Postmark and Resend success include the selected provider:

{
  "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

ConditionHTTP response
Authentication, signature, stale request, or missing scopeHTTP 401 with the Contract v1 authentication error, including Scope not allowed where applicable.
Invalid JSON, environment, recipients, body, header, sender, or attachmentHTTP 400 with the provider-specific validation error.
Caller-supplied SES fromHTTP 400, sender_identity_managed_by_gateman.
Caller-supplied provider authentication fieldHTTP 400, provider_authentication_is_managed_by_gateman.
Confirmed suppressed recipientHTTP 400, Recipient suppressed.
Suppression safety unavailableHTTP 503, Mail safety check unavailable.
Application concurrency limit/unavailabilityHTTP 429/503, app_concurrency_limit_exceeded or app_concurrency_guard_unavailable.
Production provider concurrency limit/unavailabilityHTTP 429/503, Too many concurrent provider requests or Provider concurrency guard unavailable.
Monthly allowance exhaustedHTTP 403, monthly_operation_limit_reached.
Operation accounting unavailableHTTP 503, Operation accounting unavailable.
Provider credential absent after admissionHTTP 400, SES not configured or Provider not configured, depending on provider.
Provider rejects a requestHTTP 400, provider_rejected for Postmark/Resend; SES returns its send-failure response.
Provider rate limitHTTP 429, provider_rate_limited, with provider details and possible Retry-After.
Provider or transport outcome is uncertainHTTP 409, mail_send_indeterminate, message_id: null.
Local send/capture failureProvider route-specific 400/503 failure response; do not interpret it as delivery.
Exact successful replayHTTP 200 with the retained successful response; no additional send or operation.
Retained definite-failure replayHTTP 409 with the retained failure response.
Active or indeterminate requestHTTP 409, mail_send_indeterminate.
Changed payload or environment under the same keyHTTP 409, idempotency_key_payload_conflict.
Readiness dependency failureHTTP 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

<!-- 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 -->