OAuth Client Setup
ZenSearch's per-user OAuth flows (Atlassian, GitHub, Microsoft 365, Google Workspace, Google Analytics, Zendesk) and the Slack workspace install flow need an OAuth client application registered with the upstream provider. For most providers, the platform admin pastes the resulting Client ID / Client Secret / Redirect URL into Admin → OAuth Clients, and the server hot-reloads them within 30 seconds.
This page documents the upstream-side steps for each provider.
Where to find the exact callback URL to register: open Admin → OAuth Clients, pick the provider, and fill in the Redirect URL field with your SERVER_PUBLIC_URL host. The amber Register this URL in {Provider}'s developer console box on that page derives the exact callback URL (the path differs per provider — e.g. Google Workspace uses /api/v1/integrations/google-workspace/... with a hyphen) and offers a one-click copy. Paste that value verbatim into the upstream console. Most providers validate the callback URL as an exact-match string — a trailing slash or path mismatch breaks the flow.
:::info Platform vs per-user vs per-team
"Platform OAuth client" = the upstream app itself (one set of Client ID/Secret per deployment). Each individual user then runs through the OAuth flow in-product to grant ZenSearch access to their account; that per-user grant is stored in user_connector_credentials. Platform admins set up the OAuth client once; end users authorize each time they connect.
:::
:::warning Slack is not yet wired through Admin → OAuth Clients
Slack appears in the admin UI's provider list, but as of this writing the Slack handler still reads its OAuth client from environment variables (SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, SLACK_OAUTH_REDIRECT_URL) — saving Slack credentials in Admin → OAuth Clients has no effect on the running flow. The Slack section below is included so the upstream-app registration steps live alongside the others; configure ZenSearch's side via env vars until the handler is migrated (tracked under per-user-OAuth Phase 2/3).
:::
Atlassian (Jira / Confluence)
ZenSearch uses Atlassian's OAuth 2.0 (3LO) flow. One OAuth integration covers both Jira and Confluence.
Steps
-
Open the Atlassian Developer Console and click Create → OAuth 2.0 integration.
-
Name the app (e.g.
ZenSearch) and accept the developer terms. -
On the Permissions page, scroll down to the API list and click Add next to each of:
- Jira API — then click Configure and enable:
read:jira-work,write:jira-work,read:jira-user. - Confluence API — then click Configure and enable:
read:confluence-content.all,write:confluence-content,search:confluence,read:confluence-space.summary.
Don't use the Add Marketplace or custom app modal at the top of the page — that's for enrolling third-party Marketplace apps and is unrelated to OAuth scopes.
- Jira API — then click Configure and enable:
-
Authorization → OAuth 2.0 (3LO) → set the Callback URL to the exact value shown in the admin page's amber Register this URL box. Save.
-
Settings → copy the Client ID and Secret.
-
Paste both into ZenSearch Admin → OAuth Clients → Atlassian (Jira / Confluence) along with your Redirect URL host. Save.
Required scopes
read:jira-work write:jira-work read:jira-user
read:confluence-content.all write:confluence-content search:confluence read:confluence-space.summary
offline_access
One OAuth grant covers both Jira and Confluence — users connect once and the agent's Jira tools (search, get, create, update, comment, transition issues) and Confluence tools (search pages, get/create/update pages) all use the same access token.
offline_access is appended automatically by ZenSearch's authorize URL builder and must be enabled in the Atlassian app for refresh tokens to work. Without it, users will be re-prompted to log in every hour.
Notes
- A single OAuth integration can serve any number of Atlassian sites — users pick the site at authorize time. The platform admin does not need a separate client per Atlassian Cloud site.
- Atlassian validates the callback URL as a full exact-match string (host AND path). Don't add a trailing slash.
- If you previously connected when only the Jira scopes were requested, you'll need to disconnect (Settings → Integrations → Atlassian → Disconnect) and reconnect once. Atlassian doesn't widen scopes on existing tokens.
GitHub
ZenSearch uses GitHub's OAuth App flow (not the GitHub App flow — they are different products).
Steps
- Go to GitHub Developer settings → OAuth Apps (or your organization's
Settings → Developer settings → OAuth Appspage if you want it owned by an org) and click New OAuth App. - Fill in:
- Application name: e.g.
ZenSearch - Homepage URL: your
SERVER_PUBLIC_URL - Authorization callback URL: the exact value shown in the admin page's Register this URL box.
- Application name: e.g.
- Click Register application.
- On the next screen, copy the Client ID and click Generate a new client secret, then copy the secret.
- Paste both into ZenSearch Admin → OAuth Clients → GitHub along with your Redirect URL host. Save.
Required scopes
repo
repo is the minimum that lets the agent's GitHub tools read repository metadata, code search, and issues across both public and private repos the user has access to. ZenSearch validates that GitHub returned the requested scopes and surfaces an error if the user (or org policy) drops them.
Notes
- Choose between a personal OAuth App (lives under your GitHub user) or an organization OAuth App (Settings → Developer settings under your org). Org-owned apps are easier to hand off when team membership changes — recommended for production.
- GitHub allows multiple authorization callback URLs only on GitHub Apps, not OAuth Apps. If you run separate dev/staging/prod ZenSearch deployments, register one OAuth App per deployment.
Slack
ZenSearch uses Slack's bot-token OAuth installation flow (one bot install per workspace).
Steps
-
Go to Slack API → Your Apps and click Create New App → From scratch.
-
Name the app (e.g.
ZenSearch) and pick the development workspace. (You can later distribute it to other workspaces.) -
OAuth & Permissions → Redirect URLs → add the exact value shown in the admin page's Register this URL box. Save URLs.
-
On the same page, scroll to Scopes → Bot Token Scopes and add each of:
app_mentions:readchat:writechannels:historygroups:historyim:historympim:historyusers:readusers:read.email
-
Basic Information → copy the Client ID and Client Secret under App Credentials.
-
Set the following on the core-api service and restart:
SLACK_CLIENT_ID— Client IDSLACK_CLIENT_SECRET— Client SecretSLACK_OAUTH_REDIRECT_URL— the callback URL you registered (must match exactly)
Slack does not yet read from the platform OAuth Clients DB row (see the warning at the top of this page); env vars are the source of truth for now.
Required scopes
app_mentions:read chat:write channels:history groups:history
im:history mpim:history users:read users:read.email
Notes
- For multi-workspace installs (cloud SaaS pattern), the same OAuth client serves any workspace that runs through the install flow — no need for one client per customer.
- For single-workspace self-hosting, you can skip distribution and install directly into your one workspace.
- The callback URL the admin page derives for Slack (
/api/v1/integrations/slack/auth/callback) is the correct value to paste into both the Slack app's Redirect URLs list and yourSLACK_OAUTH_REDIRECT_URLenv var.
Microsoft 365
ZenSearch uses Azure AD (Entra ID) app registration with delegated Microsoft Graph permissions.
Steps
-
Open the Azure Portal → Azure Active Directory → App registrations and click New registration.
-
Fill in:
- Name: e.g.
ZenSearch - Supported account types: choose Multi-tenant if your users span multiple tenants, or Single-tenant for a locked-down deployment.
- Redirect URI: select Web and paste the exact value shown in the admin page's Register this URL box.
- Name: e.g.
-
Click Register. On the resulting Overview page, copy:
- Application (client) ID → Client ID in ZenSearch
- Directory (tenant) ID → set
M365_OAUTH_TENANT_ID(orcommonfor multi-tenant)
-
Certificates & secrets → New client secret → copy the Value immediately (it is only shown once).
-
API permissions → Add a permission → Microsoft Graph → Delegated permissions, then add the permissions matching the surfaces you want the agent to use (only request what you need):
Surface Required Graph permissions OneDrive Files.ReadWrite.AllMail Mail.ReadWrite,Mail.SendCalendar Calendars.ReadWriteTeams ChannelMessage.Send,Channel.ReadBasic.All,Team.ReadBasic.All,Chat.ReadWriteMeetings OnlineMeetings.Read,OnlineMeetingTranscript.Read.All,OnlineMeetingArtifact.Read.All,Chat.ReadTo-Do Tasks.ReadWritePeople People.ReadSharePoint Sites.ReadWrite.AllZenSearch always appends
offline_access,openid, andUser.Readautomatically. -
If your tenant requires admin consent for any of the permissions, click Grant admin consent at the top of the API permissions page.
-
Paste Client ID and Client Secret into ZenSearch Admin → OAuth Clients → Microsoft 365 along with your Redirect URL host. Set
M365_OAUTH_TENANT_IDin.envif you need to pin a tenant.
Notes
- Multi-tenant apps must be verified by Microsoft to appear in third-party tenant consent screens — for internal-only deployments, single-tenant is simpler and avoids verification.
- The client secret has a max lifetime of 24 months; rotate before expiry.
Google Workspace
ZenSearch uses Google's OAuth 2.0 web application flow with delegated user consent.
Steps
- Open the Google Cloud Console → APIs & Services → Credentials for the project you want to own the client (create one if needed).
- OAuth consent screen → configure with your app name, support email, and developer contact. Add your domain to Authorized domains.
- Add the required Scopes to the consent screen (see table below). Save.
- Credentials → Create credentials → OAuth client ID → Application type: Web application.
- Add the exact value shown in the admin page's Register this URL box to Authorized redirect URIs. Click Create.
- Copy the Client ID and Client Secret from the popup (the secret is also visible later in the credentials list).
- Paste both into ZenSearch Admin → OAuth Clients → Google Workspace along with your Redirect URL host. Save.
- On the Google Cloud project, enable each API your scopes need (e.g. Google Drive API, Gmail API, Calendar API, Sheets API, Docs API) under APIs & Services → Library.
Required scopes
| Surface | Scope |
|---|---|
| Sheets | https://www.googleapis.com/auth/spreadsheets |
| Docs | https://www.googleapis.com/auth/documents |
| Gmail | https://www.googleapis.com/auth/gmail.modify |
| Calendar | https://www.googleapis.com/auth/calendar.events |
| Drive (write to files the user creates) | https://www.googleapis.com/auth/drive.file |
| Drive (read everything the user has) | https://www.googleapis.com/auth/drive.readonly |
ZenSearch always appends openid, https://www.googleapis.com/auth/userinfo.email, and https://www.googleapis.com/auth/userinfo.profile for identity resolution — these match what shows up on Google's consent screen and in your project's credential settings.
Notes
- An OAuth consent screen marked External in Testing mode only allows users on your Test Users list — promote to In production to allow general workspace users (Google may require app verification depending on the scope set).
- For a Workspace-only deployment, set the consent screen to Internal instead of External — no verification required, and only members of your Workspace organization can authorize.
Google Analytics
ZenSearch's Google Analytics 4 agent tools use a Google OAuth 2.0 client distinct from Google Workspace. The scope set is small (analytics.readonly + identity claims), but it MUST be a separate client and a separate Admin → OAuth Clients row — sharing the Workspace client would force every connected user to re-consent on the new scope at next refresh.
Steps
- Open the Google Cloud Console → APIs & Services → Credentials — same project as Workspace is fine; create a new client below.
- OAuth consent screen → make sure your app's user type, support email, and authorized domains are set (covered already if you've configured Workspace).
- Add the required Scopes to the consent screen (see table below). Save.
- Credentials → Create credentials → OAuth client ID → Application type: Web application. Name it something like
ZenSearch — Google Analytics. - Add the exact value shown in the admin page's Register this URL box to Authorized redirect URIs. Click Create.
- Copy the Client ID and Client Secret from the popup.
- Paste both into ZenSearch Admin → OAuth Clients → Google Analytics along with your Redirect URL host. Save.
- On the Google Cloud project, enable Google Analytics Data API and Google Analytics Admin API under APIs & Services → Library. Both are required — the Data API powers
run_ga4_report/run_ga4_realtime_report, the Admin API powerslist_ga4_properties/list_ga4_metrics_dimensions.
Required scopes
| Surface | Scope |
|---|---|
| GA4 reads (reports + property metadata + account summaries) | https://www.googleapis.com/auth/analytics.readonly |
ZenSearch always appends openid, https://www.googleapis.com/auth/userinfo.email, and https://www.googleapis.com/auth/userinfo.profile so the OAuth callback can stamp the user's email + Google subject ID into the credential row.
Notes
- Per-property access: the OAuth user must have Viewer or higher on each GA4 property they want the agent to read. ZenSearch's
list_ga4_propertiesreturns only the properties the connected user has access to. - Quotas: GA Data API has project-wide quotas (default 200 queries per minute per property, 25k per day) plus per-user concurrency limits. Heavy agent usage on a small property can hit these; bump the quota in the Cloud Console if you see 429s.
- Refresh tokens: ZenSearch requests
access_type=offline+prompt=consentso Google always returns a refresh_token. If a user re-connects and the next refresh 400s, double-check the consent screen wasn't recently switched from "External (Testing)" to "External (In production)" without re-issuing consent.
PostHog
PostHog ships with two auth paths. Pick OAuth for Cloud users (zero per-user setup); pick (or also allow) Personal API Keys for self-hosted PostHog deployments or operators who'd rather not stand up an OAuth app.
OAuth (Cloud)
PostHog's OAuth surface is the first PKCE + Dynamic Client Registration flow in ZenSearch. Two important differences from the other providers on this page:
- client_secret is OPTIONAL. PostHog's discovery doc supports
token_endpoint_auth_method=none(PKCE-only public client). Leave the secret blank and ZenSearch's exchange request authenticates via PKCE alone. If your operator setup prefers a confidential client, you can also set a secret and PostHog will acceptclient_secret_post. - The client_id is either DCR-registered or manually pasted from PostHog's developer UI. Both work; DCR is the cleaner option if you control the deployment automation.
Steps
- Register the OAuth client. Two ways:
-
Manual: sign in to PostHog Cloud at https://us.posthog.com (or https://eu.posthog.com), go to My Account → OAuth applications, click Register new app. Fill in:
- Name: e.g.
ZenSearch - Redirect URI: the exact value from the admin page's Register this URL box (something like
https://app.zensearch.example.com/api/v1/integrations/posthog/user/oauth/auth/callback). - Authorization grant type:
Authorization code - Client type:
Public(PKCE-only) is fine.Confidentialworks too — copy the secret.
- Name: e.g.
-
Dynamic Client Registration (RFC 7591): one-off
POSTto PostHog's region-agnostic registration endpoint:curl -X POST https://oauth.posthog.com/oauth/register/ \-H 'Content-Type: application/json' \-d '{"client_name": "ZenSearch","redirect_uris": ["https://app.zensearch.example.com/api/v1/integrations/posthog/user/oauth/auth/callback"],"token_endpoint_auth_method": "none","grant_types": ["authorization_code", "refresh_token"]}'The response carries
client_id. (Optionally pass"token_endpoint_auth_method": "client_secret_post"to get aclient_secretback too.)
-
- Open ZenSearch Admin → OAuth Clients → PostHog.
- Paste the Client ID. Leave Client Secret blank if your client is public; otherwise paste the secret.
- Save.
Required scopes
ZenSearch's authorize URL builder requests these scopes (all read-only):
openid profile email
query:read dashboard:read insight:read
event_definition:read property_definition:read
project:read organization:read
openid + profile + email populate the user's display name + email in the credential row. project:read + organization:read are needed for project enumeration during/after consent.
Personal API Key (PAT) — required for self-hosted
End users with self-hosted PostHog deployments don't have OAuth available (unless their operator stood it up locally). They paste a Personal API Key directly into the Settings panel:
- In PostHog, Account Settings → Personal API Keys → Create personal API key.
- Grant the read scopes listed above.
- Copy the key (shown once at creation).
- In ZenSearch, Integrations → PostHog → Use a Personal API Key. Paste the key, the numeric Project ID, choose region (or
self-hosted+ base URL).
Self-hosted base URL allowlist (operator)
For self-hosted PostHog, ZenSearch SSRF-gates the base URL with an allowlist composed from two sources:
- Operator env (read-only from the UI):
POSTHOG_ALLOWED_HOSTS— comma-separated list of hostnames or URLs. Example:POSTHOG_ALLOWED_HOSTS=posthog.acme.com,analytics.example.io. - Per-team admin rows: team admins manage their team's allowlist in the PostHog Settings panel → Self-hosted PostHog allowlist section. GET / POST / DELETE to
/api/v1/teams/:teamID/integrations/posthog/allowed-hosts(GET open to any team member; POST/DELETE require team admin).
A host passes the gate when it appears in EITHER source. The closed us.posthog.com / eu.posthog.com Cloud hosts are always allowed for Cloud regions and do NOT need an allowlist entry.
Notes
- Refresh tokens: PostHog rotates
refresh_tokenon every refresh under PKCE. ZenSearch persists the new token before writing the cache so a DB blip mid-refresh forces a clean reconnect rather than a silently-broken row. - Self-hosted + OAuth: this PR ships OAuth for Cloud only. Self-hosted PostHog OAuth (the operator stands up their own OAuth app on the self-hosted instance) is a follow-up — for now self-hosted users use PAT.
- Validate-on-save: the credentials handler calls
GET /api/projects/:id/event_definitions/?limit=1against the user's chosen region before persisting. A typoed Project ID or stale region surfaces as a clean 400 in the Settings panel rather than a delayed AuthRequired bounce on the next agent run.
Zendesk
Zendesk OAuth clients are registered per Zendesk account (subdomain), not centrally. If your ZenSearch deployment serves users across multiple Zendesk accounts, register one OAuth client in each — they can share the same Client ID string but the secret will differ per account.
Steps
- Sign in to your Zendesk Admin Center:
https://{subdomain}.zendesk.com/admin/apps-integrations/apis/zendesk-api. - Open the OAuth Clients tab → Add OAuth client.
- Fill in:
- Client Name: e.g.
ZenSearch - Description: optional
- Company: your company name
- Unique Identifier: a stable string (this becomes part of the Client ID) — pick something like
zensearch - Redirect URLs: add the exact value shown in the admin page's Register this URL box.
- Client Name: e.g.
- Save. Zendesk shows the Secret once on the next screen — copy it immediately.
- The Client ID is the Unique Identifier you chose (sometimes shown as the value of
unique_identifierin the resulting client row). - Paste both into ZenSearch Admin → OAuth Clients → Zendesk along with your Redirect URL host. Save.
Required scopes
read write hc:read
read write covers tickets and users; hc:read covers Help Center articles. ZenSearch's authorize URL builder maps the tickets and help_center short names onto these.
Notes
- The Zendesk subdomain is supplied by the end user when they connect (the connector form prompts for it), so the platform OAuth client doesn't need to be subdomain-aware — but the OAuth client must exist under the same subdomain the user types in.
- If you operate a single Zendesk account (typical for self-hosted deployments), one OAuth client is all you need.
Troubleshooting
"redirect_uri mismatch" or 400 on callback
The most common cause is the path or trailing-slash mismatch. The Register this URL box on the admin page derives the exact callback URL from the Redirect URL field — copy that value verbatim into the upstream console, including the path. If you change SERVER_PUBLIC_URL, you must update the registration in every provider's console.
Hot reload didn't pick up the new credentials
The server caches resolved OAuth client rows for 30 seconds. Wait that long, or restart the core-api service to force an immediate reload. The admin UI's Source badge flips from Not configured / From env to Configured once the server sees the new row.
"OAuth client not configured" 400 in production
Check the Source badge for the provider on Admin → OAuth Clients. If it says Not configured, the row is missing or failed to decrypt. If it says Resolution error, the saved row exists but couldn't be decrypted with the current CREDENTIAL_ENCRYPTION_KEY — re-saving with the current key clears the error.
Rotating credentials
To rotate a Client Secret: generate a new secret in the upstream console, paste it into the admin form (Client ID stays the same), Save. Existing user-issued tokens remain valid until they expire and refresh — only future authorize flows use the new secret.