OpenSequence
DocsAPI referenceSDKs and CLIReference
Senders

Connect Google

Register your own OAuth client once, then let each sender consent to Gmail and Calendar in one step.

Edit on GitHub

Each project brings its own Google OAuth client; the service owns the token lifecycle. Register the client once, then start the consent flow per sender.

curl
# once per project
curl -X PUT $SEQ/v1/project/providers/google -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"client_id":"1234-abc.apps.googleusercontent.com","client_secret":"GOCSPX-…"}'
# per sender: returns the URL the sender opens once
curl -X POST $SEQ/v1/senders/u_42/connections/google -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"return_url":"https://app.acme.com/settings/mailbox?connected=1"}'
# {"success":true,"data":{"auth_url":"https://accounts.google.com/o/oauth2/v2/auth?…","connection_id":"con_…"}}

Create the OAuth client in Google Cloud Console

In a Google Cloud project, enable the Gmail API and the Google Calendar API, configure the OAuth consent screen, and create an OAuth client of type Web application. Add the redirect URI your instance reports:

curl $SEQ/v1/project -H "Authorization: Bearer $KEY"
# data.google_redirect_uri == "https://sequences.example.com/v1/oauth/google/callback"

It is SEQ_PUBLIC_URL + /v1/oauth/google/callback.

Store the client in the project

PUT /v1/project/providers/google with client_id and client_secret. The secret is encrypted under SEQ_ENCRYPTION_KEYS and never returned: GET /v1/project shows the provider's status, key id, last four characters and timestamps. Rotate by calling the same endpoint again.

Start the consent flow for a sender

POST /v1/senders/{id}/connections/google returns auth_url and a connection_id. Send the sender's browser there. The URL asks for access_type=offline and prompt=consent, so Google issues a refresh token every time, and carries a signed state naming the project, the sender and the connection, valid for ten minutes, so a callback cannot be replayed or pointed at another sender. return_url is where the sender lands afterwards (otherwise a plain "Google connected" page).

The callback creates the email channel

Google redirects to /v1/oauth/google/callback. The auth module checks the state, exchanges the code, stores the refresh token encrypted, records the consenting account, and the service creates the sender's email channel at that address with status active. From then on readiness.email.ok is true.

The scopes

ScopeWhy
gmail.sendsend as the sender
gmail.readonlyread replies and bounces in the threads the service started (history polling)
calendar.readonlydetect bookings: events with an enrolled contact as attendee
userinfo.emaillearn which account consented, to set the channel address

gmail.send and gmail.readonly are required; a consent without them fails with 422 SCOPES_MISSING (details.missing lists them) and the sender must connect again and allow every box. Gmail scopes are "sensitive" in Google's terms: an app used by accounts outside your Workspace domain needs Google's verification before the consent screen stops warning users.

What the service reads, and never reads

Only threads the service started are read: the poll job asks Gmail's history API for new inbox messages and keeps only those whose thread or In-Reply-To matches a message it sent in the last 30 days. Everything else in the mailbox is ignored and never stored. Reply bodies are stored only when the project sets store_inbound_bodies: true. From the calendar it stores only the event id, its start and the matched attendee's email, for events with an enrolled contact as attendee, never the title, the description or the other attendees.

Token renewal and credentials_needed

Access tokens are cached per replica and refreshed two minutes before they expire, one refresh at a time per connection. When Google answers invalid_grant (the user revoked access, changed their password with certain policies, or the token aged out), the connection goes credentials_needed, the email channel too, that sender's email steps are deferred, calendar polling stops, and sender.channel_status is delivered to your endpoints. Readiness then says email_credentials_needed: start the connect flow again. Disconnecting (DELETE /v1/senders/{id}/connections/google) revokes the token at Google and sets the channel disconnected.

Pub/Sub push (optional)

Replies are found by polling Gmail history every minute. POST /hooks/google/{slug} exists for a Pub/Sub push subscription and is acknowledged, but it is only a nudge: the poller still does the reading. Registering a watch and the pubsub_topic setting are not wired yet; the minute poll is the mechanism today.

With mock providers

SEQ_DEV_MOCK_PROVIDERS=1 skips Google entirely: the connect call answers { "status": "active", "mock": true, "channel": … } and the mailbox is active at the sender's email (or <id>@mock.local).

On this page