On this page
Guides / Third-party OAuth apps

Third-party OAuth apps

Connect your app with user authorization, PKCE S256 and rotating tokens, without asking for API keys.

Use OAuth when your application must operate a workspace owned by someone else. The person authorizes the scopes they see in Meteor; your app receives a short-lived access token. Never ask for or capture their API key.

Meteor implements OAuth 2.0 Authorization Code with mandatory PKCE S256. The access token is a Bearer token for the same public API and lasts one hour. The refresh token lasts 30 days and rotates on every use.

Before you redirect

In the workspace that owns your integration, open Settings → Developers → OAuth apps and register the app. Store its client_id. If you choose a confidential app, also store the client_secret: Meteor shows it only on creation or rotation.

Generate an unpredictable state and a PKCE code_verifier between 43 and 128 characters for each user. Store both in your app session. Compute code_challenge = base64url(SHA-256(code_verifier)), then redirect the browser to Meteor's consent screen:

import { createHash, randomBytes } from 'node:crypto';
import { OAuth } from '@meteor.ia/sdk';

const verifier = randomBytes(48).toString('base64url');
const challenge = createHash('sha256').update(verifier).digest('base64url');
const state = randomBytes(24).toString('base64url');

// Store verifier and state in the session before redirecting.
const oauth = new OAuth();
const url = oauth.authorizeUrl({
  clientId: process.env.MET_OAUTH_CLIENT_ID!,
  redirectUri: 'https://your-app.example.com/oauth/callback',
  scope: ['runs:execute', 'contacts:read'],
  state,
  codeChallenge: challenge,
});
res.redirect(url);

The person signs in to Meteor if needed and sees your app name, website, and requested scopes. If they accept, Meteor redirects to your redirect_uri with code and the same state. If they deny, it returns with error=access_denied.

Exchange the code on your server

In the callback, validate state against what you stored, then exchange the code. Always send the identical redirect_uri and the original code_verifier. The code is single use and expires after 60 seconds.

const { access_token, refresh_token, expires_in, scope } = await oauth.exchangeCode({
  clientId: process.env.MET_OAUTH_CLIENT_ID!,
  clientSecret: process.env.MET_OAUTH_CLIENT_SECRET, // confidential apps only
  code: String(req.query.code),
  redirectUri: 'https://your-app.example.com/oauth/callback',
  codeVerifier: session.pkceVerifier,
});

// Encrypt refresh_token when storing it. access_token is a short-lived met_ key.

Do not send the client_secret, code_verifier, authorization code, or refresh token to a browser, log, or analytics provider. The token does not include a workspace_id: preserve the workspace your integration operates in your own installation record if the API route needs it.

Call the API and refresh

access_token is a Bearer token for the approved scopes. Use it with the REST API or SDK; set the installation's workspaceId for routes that require it.

import Met from '@meteor.ia/sdk';

const met = new Met(access_token, { workspaceId: installation.workspaceId });
const run = await met.runs.create({ input: 'Summarize today\'s leads' });

Before it expires, or after an expired-token response, refresh from your server. Every refresh issues a new refresh token: replace the previous one atomically. Reusing a consumed refresh token revokes the whole grant for safety.

const renewed = await oauth.refresh({
  clientId: process.env.MET_OAUTH_CLIENT_ID!,
  clientSecret: process.env.MET_OAUTH_CLIENT_SECRET,
  refreshToken: installation.refreshToken,
});
// Store renewed.access_token and renewed.refresh_token together.

Revoke and rotate

A person can revoke your app from Settings → Developers → OAuth apps. Your app can also revoke an access or refresh token; revoking a refresh token invalidates the whole grant:

await oauth.revoke(installation.refreshToken, {
  clientId: process.env.MET_OAUTH_CLIENT_ID!,
  clientSecret: process.env.MET_OAUTH_CLIENT_SECRET,
});

For confidential apps, rotate the client_secret from the same screen. A normal rotation keeps the previous secret valid for 24 hours while you deploy the new one; use immediate invalidation only after a leak.

If your owning workspace already uses outbound webhooks, you can subscribe to app.authorized and app.revoked with integrations:read. Those events go to the workspace that registered the app and include client_id, workspace_id, scopes, and grant_id, without personal data.

Current limits

The authorization server does not yet publish OAuth metadata at /.well-known/oauth-authorization-server or dynamic client registration. Configure the URLs documented in this guide manually; do not attempt to discover them from the domain.