Sending Email through Gmail¶
The gmail transport sends email through the Gmail REST API (messages.send), from a Gmail or Google Workspace mailbox that has granted pg_relay a one-time OAuth2 consent. See SMTP for the alternative path into the same mailbox — Gmail and Google Workspace also accept ordinary SMTP, reachable through the SMTP transport with either an App Password or the same OAuth2 refresh-token grant this chapter uses.
When to use this transport¶
Unlike Azure Communication Services, Google has no purpose-built high-volume transactional email product — Gmail and Google Workspace enforce daily sending caps meant for normal mailbox use. This transport is for sending notifications through an existing Gmail or Workspace mailbox: a shared team inbox, a service account's own mailbox, a small-scale internal notifier. For bulk or high-volume transactional sending from a verified custom domain, use Azure Communication Services instead.
Profile fields¶
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
oauth2 |
object | yes | — | the refresh-token grant — see below |
from |
string | yes | — | the sender address; must be the consenting mailbox itself, or one of the Send As aliases configured in that mailbox's own Gmail settings |
timeout_seconds |
integer | no | 30 (capped at 120) |
The oauth2 block¶
The credential here is a refresh token — the durable result of a one-time OAuth consent performed by the sending mailbox. There is no tenant and no service-wide secret: one refresh token authorises exactly one mailbox, and a deployment that sends from three mailboxes holds three refresh tokens.
| Field | Required | Notes |
|---|---|---|
token_url |
yes | Google's token endpoint: https://oauth2.googleapis.com/token |
client_id |
yes | the Google Cloud OAuth client id (....apps.googleusercontent.com) |
client_secret |
yes | use _env:VAR_NAME — see Keeping Secrets Out of the Database |
refresh_token |
yes | use _env:VAR_NAME — the mailbox's own consent grant |
The short-lived access tokens exchanged from the refresh token are cached per (client_id, refresh_token) — the same shared token cache every OAuth2-authenticated transport uses — and refreshed automatically on expiry. A cached token that gets rejected (an HTTP 401) triggers exactly one automatic refresh and retry before the send is given up on.
The refresh token itself never expires on a schedule; it stays valid until the mailbox owner (or a Workspace admin) revokes it. If it is revoked, Google's token endpoint answers invalid_grant, the send resolves as a permanent failure, and an operator re-runs the OAuth consent flow for that mailbox and updates the referenced environment variable.
Message fields¶
| Field | Required | Notes |
|---|---|---|
to |
at least one address | |
cc |
no | |
bcc |
no | carried as a Bcc header of the built message (the Gmail API has no separate envelope); Gmail delivers to those recipients without exposing the header to the To/Cc copies |
reply_to |
no | |
subject |
no | |
body_text / body_html |
at least one | supplying both sends multipart/alternative, exactly as the SMTP transport does |
attachments |
no | array of {filename, content_type, content_base64}; the Gmail API caps the total encoded message at 25 MB |
Under the hood, this transport builds the same MIME message the SMTP transport builds — same multipart structure, same quoted-printable and base64 encodings, same deterministic Message-ID (see SMTP — Message-ID and duplicate-resistant retries) — and hands it to the API base64url-encoded. What a recipient receives is byte-for-byte the message an SMTP send would have produced.
The sender is the mailbox¶
The Gmail API always sends as the mailbox that granted the OAuth consent — the request is addressed to users/me, the API's fixed alias for "whichever mailbox this token belongs to". from must therefore be that mailbox's own address, or a Send As alias the mailbox owner has configured and verified in Gmail's own settings. This is not a free-form sender field the way a verified-domain service allows: one credential, one mailbox. Sending as arbitrary mailboxes across a Workspace domain (domain-wide delegation) is deliberately not supported — grant each sending mailbox its own consent instead.
What success means¶
Unlike Azure Communication Services' asynchronous acceptance, messages.send is synchronous: a 200 response means Gmail has sent the message, and the response carries the sent message's own id, which becomes the provider reference recorded against the notification. There is no later operation to poll and no acceptance-versus-delivery gap to reason about — the id refers to the actual message, findable in the sending mailbox's Sent view.
Failure classification¶
A 429 response (rate limiting) is transient and goes through the normal retry chain, as are 5xx responses and outright network failures. So is a 403 — unusually among the email transports, because Gmail signals quota exhaustion (its per-day sending caps, and per-user rate limits) as 403, and those genuinely recover once the window resets. Every other 4xx — a malformed message, a revoked consent, an unknown mailbox — is a permanent failure, resolved immediately with no retry.
Daily sending caps
Gmail and Google Workspace enforce per-day sending quotas. On a day the quota runs out, sends fail transiently and retry on the normal backoff chain — which, depending on the channel's max_retries, may be exhausted before the quota resets. If your volume is anywhere near the mailbox's daily cap, this is the signal to move that traffic to a transactional service such as Azure Communication Services.
Continue to Sending to a Webhook.