Skip to content

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.