Supyagent
Connected Accounts

OAuth Connect

Initiate hosted OAuth flows for connected accounts — Supyagent handles PKCE, token exchange, and encrypted storage.

OAuth Connect

Connect sessions are the mechanism for connecting a provider (Google, Slack, etc.) to a connected account. Supyagent fully hosts the OAuth flow — you initiate a session, redirect the user to the provider, and Supyagent handles the token exchange, encryption, and storage.

Initiating a Connect Session

POST /api/v1/accounts/:id/connect

Creates a connect session and returns an OAuth authorization URL.

Request Body

FieldTypeRequiredDescription
providerstringYesOAuth provider to connect (see supported providers)
redirect_urlstringYesURL where the user returns after authorization
scopesstring[]NoCustom OAuth scopes (defaults vary by provider)
curl -X POST https://app.supyagent.com/api/v1/accounts/550e8400-e29b-41d4-a716-446655440000/connect \
  -H "Authorization: Bearer sk_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "google",
    "redirect_url": "https://yourapp.com/oauth/complete",
    "scopes": [
      "https://www.googleapis.com/auth/gmail.readonly",
      "https://www.googleapis.com/auth/calendar.readonly"
    ]
  }'

Response

{
  "ok": true,
  "data": {
    "connect_url": "https://accounts.google.com/o/oauth2/v2/auth?client_id=...&state=abc123&code_challenge=xyz...",
    "session_id": "660e8400-e29b-41d4-a716-446655440001",
    "expires_at": "2026-02-25T10:30:00.000Z"
  }
}
FieldDescription
connect_urlThe OAuth authorization URL to open in the user's browser
session_idSession identifier for polling status
expires_atSession expiry time (30 minutes from creation)

Errors

StatusErrorMeaning
400Validation errorInvalid provider or missing redirect_url
403PARTNER_REQUIREDNo active partner profile
404Not foundConnected account doesn't exist or doesn't belong to you

The OAuth Flow

After you receive the connect_url, here's what happens:

Step 1: Redirect the User

Open the connect_url in the user's browser. Common patterns:

// Full-page redirect
window.location.href = connectUrl;

// Popup window
window.open(connectUrl, 'oauth', 'width=600,height=700');

// Link/button
<a href={connectUrl}>Connect Google</a>

Step 2: User Authorizes

The user sees the provider's standard OAuth consent screen (e.g., "Allow Acme App to access your Gmail?"). If they approve, the provider redirects to Supyagent's callback.

Step 3: Token Exchange (Automatic)

Supyagent's callback handler (/api/v1/connect/callback) automatically:

  1. Validates the CSRF state parameter
  2. Checks the session hasn't expired (30-minute window)
  3. Decrypts the stored PKCE code verifier
  4. Exchanges the authorization code for access and refresh tokens
  5. Fetches basic user info from the provider (email, name)
  6. Encrypts and stores the tokens in the integrations table
  7. Auto-enables services based on the provider's permission registry
  8. Marks the connect session as completed

Step 4: Redirect Back

The user is redirected to your redirect_url with query parameters:

Success:

https://yourapp.com/oauth/complete?status=success&provider=google&account_id=user-123

Failure:

https://yourapp.com/oauth/complete?error=access_denied

The account_id parameter contains the connected account's external_id, so you can immediately identify which user completed the flow.


Checking Session Status

GET /api/v1/accounts/:id/connect/:sessionId

Poll this endpoint to check the status of a connect session. Useful for popup-based flows where you can't rely on the redirect.

curl https://app.supyagent.com/api/v1/accounts/550e8400-e29b-41d4-a716-446655440000/connect/660e8400-e29b-41d4-a716-446655440001 \
  -H "Authorization: Bearer sk_live_your_key_here"

Response

{
  "ok": true,
  "data": {
    "session_id": "660e8400-e29b-41d4-a716-446655440001",
    "provider": "google",
    "status": "completed",
    "created_at": "2026-02-25T10:00:00.000Z",
    "expires_at": "2026-02-25T10:30:00.000Z"
  }
}

Session Statuses

StatusMeaning
pendingSession created, waiting for user to authorize
completedOAuth flow finished successfully. Tokens stored.
expiredSession exceeded the 30-minute window
failedAn error occurred. Check the error field for details.

When the status is failed, an additional error field is included:

{
  "status": "failed",
  "error": "access_denied"
}

Supported Providers

ProviderDefault Scopes
googleGmail, Calendar, Drive, Docs, Sheets, Slides (all Google scopes)
slack— (Slack determines scopes from app config)
discordStandard user scopes
githubread:user, user:email, repo
microsoftMail, Calendar, OneDrive, User
linkedinProfile, posts
twitterStandard user scopes with PKCE
notionWorkspace access
hubspotCRM access
jiraread:jira-work, write:jira-work, read:jira-user
linearread, write
calendlyEvent types, scheduled events
salesforceapi, refresh_token
pipedriveStandard CRM access

Custom Scopes

You can override the default scopes for any provider by passing a scopes array:

{
  "provider": "google",
  "redirect_url": "https://yourapp.com/callback",
  "scopes": [
    "https://www.googleapis.com/auth/gmail.readonly"
  ]
}

This is useful when you only need a subset of permissions and want to request the minimum required scope from the user.


Security Details

PKCE (Proof Key for Code Exchange)

Every connect session generates a PKCE pair:

  • Code verifier — random secret, encrypted and stored in the session
  • Code challenge — SHA256 hash of the verifier, sent in the authorization URL

This prevents authorization code interception attacks, even if the connect_url is exposed.

CSRF Protection

Each session has a unique state parameter (32 random bytes). The callback validates that the state matches an existing pending session before processing. This prevents cross-site request forgery.

Token Encryption

All OAuth tokens (access and refresh) are encrypted with AES before storage. They are decrypted only at request time when making API calls on behalf of the connected account.


Implementation Patterns

Redirect-Based Flow

The simplest pattern — redirect the user and handle the callback on your server:

// 1. Your backend: create connect session
const res = await fetch('https://app.supyagent.com/api/v1/accounts/${accountId}/connect', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer sk_live_...',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    provider: 'google',
    redirect_url: 'https://yourapp.com/integrations/callback',
  }),
});
const { data } = await res.json();

// 2. Redirect user to data.connect_url
// 3. Handle callback at /integrations/callback

Open OAuth in a popup and poll for completion:

// 1. Create session (same as above)
const { data } = await createConnectSession(accountId, 'google');

// 2. Open popup
const popup = window.open(data.connect_url, 'oauth', 'width=600,height=700');

// 3. Poll for completion
const interval = setInterval(async () => {
  const status = await checkSessionStatus(accountId, data.session_id);

  if (status.data.status === 'completed') {
    clearInterval(interval);
    popup?.close();
    // Refresh your UI to show the new integration
  } else if (status.data.status === 'failed' || status.data.status === 'expired') {
    clearInterval(interval);
    popup?.close();
    // Show error to user
  }
}, 2000); // Poll every 2 seconds

OAuth Redirect URI Configuration

For connect sessions to work, your OAuth app at each provider must include the following callback URL in its allowed redirect URIs:

https://app.supyagent.com/api/v1/connect/callback

This is separate from the standard dashboard OAuth callbacks. Both must be registered.

EnvironmentCallback URL
Localhttp://localhost:3000/api/v1/connect/callback
Productionhttps://app.supyagent.com/api/v1/connect/callback

Some providers (e.g., GitHub) only support one callback URL per OAuth app. In these cases, you may need separate OAuth apps for dashboard auth and connect sessions.

What's Next