Imperal Docs
SDK Reference

ext.oauth reference

ext.oauth + ctx.oauth_authorize_url — let the platform run the whole OAuth connect flow: declare a provider, build the authorize URL, save accounts automatically.

ext.oauth(...) hands the entire OAuth "connect an account" flow to the platform. You declare which providers your extension connects (Google, Microsoft, Yahoo, …), and the platform runs the dance end to end: it builds the authorize URL, exchanges the code, fetches the account email, saves a standard account record, and renders the result window. You write no OAuth code — no webhook, no token-exchange, no cron, no profile call.

This replaces the old pattern of hand-rolling an @ext.webhook("callback") + a polling schedule. Don't do that anymore — it's error-prone (scope mistakes, raw-JSON windows, deferred account creation) and duplicates what the platform already does.

The two pieces

  1. Declare the provider (once, at module scope):
from imperal_sdk import Extension

ext = Extension("mail", version="1.0.0")

ext.oauth(
    "google",
    collection="gmail_accounts",
    scopes=["https://www.googleapis.com/auth/gmail.modify"],
)
  1. Return the authorize URL from your connect handler:
@chat.function("connect_account", action_type="write")
async def connect_account(ctx, params):
    url = await ctx.oauth_authorize_url(params.provider)   # "google" | "microsoft" | "yahoo"
    return ActionResult.ok(data={"authorize_url": url})

That's it. The user's browser opens url, authorizes, and the platform redirects to its own callback, saves the account, and bounces the user back to your panel.

What the platform does (the callback)

The platform owns one generic route — GET /v1/ext/{app_id}/oauth/{provider}/callback — for every extension. On the provider's redirect it:

  1. verifies the signed state;
  2. resolves your client credentials from this app's app-scope secrets {provider}_client_id / {provider}_client_secret (never from env);
  3. exchanges the code for tokens at the provider's token endpoint;
  4. resolves the account email. One provider serves extensions with very different scope sets, so the platform tries that provider's identity endpoints in order and takes the first that answers — for google, OIDC userinfo first (covered by openid/email), then Gmail getProfile (for apps that declare gmail.* but not openid). Microsoft uses Graph /me, Yahoo its userinfo;
  5. saves a standard account record to your collectiononly if the email actually resolved. If no endpoint answers, the callback fails visibly instead of saving a placeholder account your extension would have to filter out later;
  6. renders a branded "connected" page that auto-redirects to https://panel.imperal.io/ext/{app_id}?connected=1.

The account is saved synchronously — it appears the moment the user lands back on your panel. No cron, no "appears within a minute".

ext.oauth(provider, *, collection=None, scopes=None)

argmeaning
providerone of the built-in providers: "google", "microsoft", "yahoo".
collectionthe store collection the account record is written to. Defaults to f"{provider}_accounts". Use one shared collection (e.g. "gmail_accounts") if your extension lists all providers' accounts together.
scopesthe OAuth scopes to request for this provider.

The standard saved record is {email, provider, access_token, refresh_token, expires_at, is_active} — the first account for a user is set active; later ones are inactive until the user switches.

await ctx.oauth_authorize_url(provider, *, login_hint=None)

Builds the provider authorize URL. Reads the public client_id from your app-scope secret, the scopes from your ext.oauth(...) declaration, sets redirect_uri to the platform callback, and attaches a signed state. Return it from your connect() handler — never hardcode scopes, redirect_uri, or client_id.

Required setup (owner / operator, once)

  1. Save the client credentials as app-scope secrets in the Developer Portal → Secrets: {provider}_client_id and {provider}_client_secret (e.g. google_client_id). Declare them with @ext.secret(scope="app") so one developer-owned key serves every user.
  2. Register the redirect URI in the provider's developer console (Google Cloud, Azure AD, Yahoo): https://panel.imperal.io/v1/ext/{app_id}/oauth/{provider}/callback. A mismatch yields redirect_uri_mismatch.

Token refresh

For ongoing API calls, refresh tokens with the same app-scope client creds you declared:

client_id = await ctx.secrets.get("google_client_id")        # app-scope: shared, owner-set
client_secret = await ctx.secrets.get("google_client_secret")
# ...POST to the provider token endpoint with grant_type=refresh_token...

Google returns a refresh_token on the first consent only — a later re-connect of the same account usually comes back without one. The platform therefore leaves the stored refresh_token untouched when a re-connect does not carry a new one, so reconnecting never downgrades a working account. Treat a missing refresh_token on re-connect as normal, not as an error.

Don't

  • Don't add a provider scope you do not otherwise need just to make the email resolvable. The platform tries each identity endpoint a provider knows about and takes the first that answers, so a Drive-only or Analytics-only app needs no gmail.* scope — openid/email is enough. Every extra scope you request is one more permission the user must approve on the consent screen.
  • Don't put client creds in env or in code — they live in app-scope secrets.
  • Don't build the callback yourself with @ext.webhook — the platform owns /v1/ext/{app_id}/oauth/{provider}/callback.

On this page