temperDocs
Menu

Guides

Bring your own Google or Slack OAuth app

Register your own OAuth app with Google or Slack so end users see your app's name on the consent screen, and what the provider's review requires.

When an end user connects Gmail, Google Calendar or Slack, they approve access on the provider's consent screen. That screen shows the name, logo and privacy policy of an OAuth app. Before you launch, that app should be yours: you register an OAuth app with Google or Slack, give Temper its client ID and secret, and from then on every authorization link Temper generates for you uses it. Temper still handles the OAuth flow, stores the tokens encrypted, and keeps them out of the VM (see credentials).

Until you register an app, Temper uses a platform test app for that provider if one is configured. The test app is in Google's and Slack's testing mode: only listed test users can authorize it, so it is for development, not for your real users. If you have not registered an app and no test app is configured, authorizing a connection fails with 409 no_oauth_client.

What Temper asks for

The scopes depend on which connectors you request in one authorization. Temper asks for exactly these, nothing more:

ConnectorProviderScopes
gmailGooglehttps://www.googleapis.com/auth/gmail.readonly, https://www.googleapis.com/auth/gmail.send
calendarGooglehttps://www.googleapis.com/auth/calendar.events.owned
slackSlackuser token scopes channels:history, channels:read, groups:history, groups:read, chat:write, search:read

Every Google authorization also asks for openid and email, so the connection can show which Google account the user connected. For Google, Temper asks for offline access (a refresh token) and always shows the consent screen. For Slack, the scopes are user token scopes: the agent acts as the user who authorized, not as a bot.

You cannot add or remove scopes; they are set by the connectors. Configure the same list on your app so the provider's review matches what your users are asked for.

What each scope is used for, in words you can adapt for your review submission:

  • gmail.readonly: the agent searches the user's mailbox and reads message bodies when the user asks it to find, summarize or reply to an email. gmail.metadata is not enough because it excludes the body. Nothing is modified or deleted, so gmail.modify is not requested. One-time codes and password reset or sign-in links are redacted before the agent sees a message.
  • gmail.send: the agent sends or replies to an email the user asked for. Sending is a high-risk action, so each email goes through approval before it is sent.
  • calendar.events.owned: the agent lists upcoming events and, when asked, creates or deletes events on the user's primary calendar. It only touches calendars the user owns, so the broader calendar.events is not requested. Deleting needs approval.
  • channels:read, groups:read: list the public and private channels the user is in.
  • channels:history, groups:history: read recent messages in a channel the user names.
  • search:read: search messages across the user's channels when the user asks.
  • chat:write: post a message the user asked for, after approval.

Register the app with the provider

The redirect URI to add on the provider's side is your control plane's base URL followed by the callback path:

<control plane base URL>/v1/oauth/google/callback
<control plane base URL>/v1/oauth/slack/callback

You don't have to build it yourself: registering the app with Temper (next section) returns the exact redirect_uri, and list OAuth clients returns it for both providers even before you register anything. The redirect URI must match exactly, or the provider rejects the authorization.

Google

  1. In the Google Cloud Console, create a project (or pick one) and enable the Gmail API and/or the Google Calendar API.
  2. Configure the OAuth consent screen (Google Auth Platform): user type External, your app name, logo, support email, home page and privacy policy links.
  3. Under data access, add the scopes from the table above for the connectors you use.
  4. Create an OAuth client of type Web application and add the Google redirect URI under Authorized redirect URIs.
  5. Copy the client ID and client secret.

While the app's publishing status is Testing, only the test users you list can authorize, and their authorization expires after 7 days. Add yourself and your testers as test users while you build.

Slack

  1. Create a Slack app at api.slack.com.
  2. Under OAuth & Permissions, add the Slack redirect URI under Redirect URLs.
  3. Under User Token Scopes, add the Slack scopes from the table above. Temper does not use a bot token, so you don't need bot scopes.
  4. To let users from other workspaces install it, enable distribution under Manage Distribution.
  5. Copy the client ID and client secret from Basic Information.

Register the app with Temper

Send the client ID and secret with PUT /v1/oauth-clients/{provider}. Calling it again replaces the app. The secret is stored encrypted and is never returned by any endpoint.

import { Temper } from "@temper-hq/sdk";

const temper = new Temper();

const app = await temper.connections.setOAuthClient("google", {
  client_id: process.env.GOOGLE_OAUTH_CLIENT_ID!,
  client_secret: process.env.GOOGLE_OAUTH_CLIENT_SECRET!,
});
console.log("Add this redirect URI to your Google OAuth client:", app.redirect_uri);

// One entry per provider: your client_id (or null), whether the platform test app is available, and the redirect URI.
for (const c of await temper.connections.listOAuthClients()) {
  console.log(c.provider, c.client_id ?? "(platform test app)", c.platform_app_available, c.redirect_uri);
}
import os

from temper_hq import Temper

temper = Temper()

app = temper.connections.set_oauth_client(
    "google",
    client_id=os.environ["GOOGLE_OAUTH_CLIENT_ID"],
    client_secret=os.environ["GOOGLE_OAUTH_CLIENT_SECRET"],
)
print("Add this redirect URI to your Google OAuth client:", app.redirect_uri)

# One entry per provider: your client_id (or None), whether the platform test app is available, and the redirect URI.
for c in temper.connections.list_oauth_clients():
    print(c.provider, c.client_id or "(platform test app)", c.platform_app_available, c.redirect_uri)
package main

import (
	"context"
	"fmt"
	"log"
	"os"

	temper "github.com/temper-hq/sdk-go"
)

func main() {
	ctx := context.Background()
	client, err := temper.New()
	if err != nil {
		log.Fatal(err)
	}

	app, err := client.Connections.SetOAuthClient(ctx, "google",
		os.Getenv("GOOGLE_OAUTH_CLIENT_ID"), os.Getenv("GOOGLE_OAUTH_CLIENT_SECRET"))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("Add this redirect URI to your Google OAuth client:", app.RedirectURI)

	// One entry per provider: your client ID (or nil), whether the platform test app is available, and the redirect URI.
	clients, err := client.Connections.ListOAuthClients(ctx)
	if err != nil {
		log.Fatal(err)
	}
	for _, c := range clients {
		id := "(platform test app)"
		if c.ClientID != nil {
			id = *c.ClientID
		}
		fmt.Println(c.Provider, id, c.PlatformAppAvailable, c.RedirectURI)
	}
}

For Slack, pass "slack" as the provider.

Connect an end user

Create an authorization link with POST /v1/connections/authorize and send the end user to it. The link expires after 10 minutes and works once. All connectors in one call must belong to the same provider: gmail and calendar (Google), or slack (Slack).

import { Temper } from "@temper-hq/sdk";

const temper = new Temper();

const link = await temper.connections.authorize({
  end_user_id: "alice",
  connectors: ["gmail", "calendar"],
  return_url: "https://app.example.com/settings/connections",
});
// Redirect the end user's browser to link.url (valid until link.expires_at, single use).
console.log(link.url);
from temper_hq import Temper

temper = Temper()

link = temper.connections.authorize(
    end_user_id="alice",
    connectors=["gmail", "calendar"],
    return_url="https://app.example.com/settings/connections",
)
# Redirect the end user's browser to link.url (valid until link.expires_at, single use).
print(link.url)
link, err := client.Connections.Authorize(ctx, temper.AuthorizeConnectionParams{
	EndUserID:  "alice",
	Connectors: []string{"gmail", "calendar"},
	ReturnURL:  "https://app.example.com/settings/connections",
})
if err != nil {
	log.Fatal(err)
}
// Redirect the end user's browser to link.URL (valid until link.ExpiresAt, single use).
fmt.Println(link.URL)

The user sees your app on the provider's consent screen. After they choose, the provider sends them to Temper's callback, Temper exchanges the code for tokens, and then redirects the browser to your return_url with the result added to the query string:

https://app.example.com/settings/connections?status=ok&connection_id=conn_...
https://app.example.com/settings/connections?status=error&error=access_denied

status=error covers the user declining and a failed token exchange; error carries the reason. If the link expired or was already used, Temper cannot tell where to send the user and shows a short error page instead; create a new link.

Authorizing again for the same provider replaces the end user's previous connection for that provider: the old one is revoked and its stand-in tokens stop working. Ask for every connector you need from a provider in one authorization, for example ["gmail", "calendar"], rather than one at a time.

With a webhook configured, you also receive connection.created (with replaced, the ID of the connection it replaced, if any), connection.revoked, and connection.error. A connection moves to status error when the provider rejects a token refresh, usually because the user revoked access on the provider's side; send them through authorization again.

List and revoke connections

Responses never include tokens. Revoking also revokes the token with the provider where possible, and returns the revoked connection; revoking an already revoked connection returns it unchanged.

const { data } = await temper.connections.list({ end_user_id: "alice" });
for (const c of data) console.log(c.id, c.provider, c.account, c.connectors.join(","), c.status);

await temper.connections.revoke(data[0]!.id);
conns = temper.connections.list(end_user_id="alice")
for c in conns.data:
    print(c.id, c.provider, c.account, ",".join(c.connectors), c.status)

temper.connections.revoke(conns.data[0].id)
conns, err := client.Connections.List(ctx, "alice", false) // false: leave out revoked connections
if err != nil {
	log.Fatal(err)
}
for _, c := range conns.Data {
	fmt.Println(c.ID, c.Provider, c.Status)
}
if len(conns.Data) > 0 {
	if _, err := client.Connections.Revoke(ctx, conns.Data[0].ID); err != nil {
		log.Fatal(err)
	}
}

Provider review

Your app is reviewed by the provider under your name. Plan for this early; it can take weeks.

Google

  • gmail.readonly is a restricted scope. A public app that requests it must pass Google's verification, including restricted scope verification, and a CASA security assessment by a Google-authorized assessor. The assessment is repeated every year and is paid. See restricted scope verification and security assessment.
  • gmail.send is a sensitive scope and also needs verification.
  • Until your app is verified, users see an "unverified app" warning on the consent screen, and the app is capped at 100 users.
  • Data from these scopes falls under Google's user data policy, including the Limited Use requirements. Make sure your privacy policy covers it.

Temper runs the infrastructure that holds the tokens and calls Google, so your assessment will include questions about it. The security overview describes what Temper protects against.

Slack

  • Since 2025-05-29, commercially distributed apps that are not approved for the Slack Marketplace are limited to 1 request per minute, and at most 15 messages per request, for conversations.history and conversations.replies. Internal apps used only in your own workspace are not affected. See the Slack changelog.
  • In practice, this means the agent can read very little channel history until your app is in the Marketplace.
  • The Marketplace has its own requirements, including least-privilege scopes and disclosures for AI apps. See the Slack Marketplace app guidelines and requirements.