Last updated

OAuth Account API Integration Guide

This documentation covers OAuth integration for both Google and Microsoft accounts.

Overview

This API allows third-party developers to programmatically connect Google OAuth accounts to Instantly workspaces without requiring browser automation through the frontend.

Flow Summary

  1. Send an API request to get the OAuth URL
    • Send this request with the API key of the workspace to which you want to connect the account.
  2. Open that URL in a new browser tab and connect the account
    • The account will automatically connect behind the scenes
    • There is no need for any Instantly login cookie, or to open the Instantly dashboard at all
    • [Optionally] You can check status using the session ID
      • you get the session ID from step #1

Authentication

All API requests require authentication using your Instantly API key:

Authorization: Bearer <your-api-key>

The API key must have the accounts:create scope for initializing OAuth and accounts:read scope for checking status.

Provider-Specific Notes

The API endpoints and flow are identical - simply replace the provider name in the Init endpoint:

ProviderInit EndpointStatus Endpoint [same for both]
GooglePOST /api/v2/oauth/google/initGET /api/v2/oauth/session/status/:sessionId
MicrosoftPOST /api/v2/oauth/microsoft/initGET /api/v2/oauth/session/status/:sessionId

Google

  • Only Google Workspace (GSuite) accounts are supported
  • Personal Gmail accounts (@gmail.com) will be rejected

Microsoft

  • Supports both Microsoft 365 (business) and personal Microsoft accounts
  • Uses Outlook/Office 365 for email access

Note
All code examples below use Google endpoints. To use Microsoft instead, simply replace google with microsoft in all URLs.

API Endpoints

1. Initialize OAuth Session

Creates a new OAuth session and returns the Google authorization URL.

Endpoint: POST /api/v2/oauth/google/init

Request:

curl -X POST "https://api.instantly.ai/api/v2/oauth/google/init" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json"

Response:

{
  "session_id": "550e8400-e29b-41d4-a716-446655440000",
  "auth_url": "https://accounts.google.com/o/oauth2/v2/auth?client_id=...&redirect_uri=...&state=api_session:550e8400-e29b-41d4-a716-446655440000&...",
  "expires_at": "2024-01-15T10:30:00.000Z"
}
FieldTypeDescription
session_idstringUnique session ID for polling status
auth_urlstringGoogle authorization URL to open in browser
expires_atstring (ISO 8601)Session expiry time (10 minutes from creation)

2. Check OAuth Status

Poll this endpoint to check the result of the OAuth flow.

Endpoint: GET /api/v2/oauth/session/status/:sessionId

Request:

curl "https://api.instantly.ai/api/v2/oauth/session/status/550e8400-e29b-41d4-a716-446655440000" \
  -H "Authorization: Bearer YOUR_API_KEY"

Response (Pending):

{
  "status": "pending"
}

Response (Success):

{
  "status": "success",
  "email": "user@example.com",
  "name": "John Doe",
  "account_id": "user@example.com"
}

Response (Error):

{
  "status": "error",
  "error": "account_exists",
  "error_description": "Account already exists in another workspace"
}

Response (Expired):

{
  "status": "expired"
}
StatusDescription
pendingUser has not completed OAuth yet
successAccount successfully connected
errorOAuth failed (see error fields)
expiredSession expired (10 minute timeout)

Integration Guide

Step 1: Initialize the OAuth Session

Call the init endpoint to get a session ID and authorization URL.

const response = await fetch('https://api.instantly.ai/api/v2/oauth/google/init', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${API_KEY}`,
    'Content-Type': 'application/json'
  }
});

const { session_id, auth_url, expires_at } = await response.json();

Step 2: Open the Authorization URL

Direct the user to the auth_url to complete Google authentication. This can be done by:

  • Option A: Opening in a new browser tab/window
  • Option B: Opening in a popup window
  • Option C: Redirecting the current page (requires handling the return)
// Option A: New tab
window.open(auth_url, '_blank');

// Option B: Popup window
const popup = window.open(
  auth_url,
  'google-oauth',
  'width=500,height=600,scrollbars=yes'
);

Step 3: Poll for Completion

Poll the status endpoint until the session completes or expires.

async function pollOAuthStatus(sessionId, apiKey, maxAttempts = 60) {
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    const response = await fetch(
      `https://api.instantly.ai/api/v2/oauth/session/status/${sessionId}`,
      {
        headers: { 'Authorization': `Bearer ${apiKey}` }
      }
    );
    
    const result = await response.json();
    
    if (result.status === 'success') {
      return { success: true, account: result };
    }
    
    if (result.status === 'error') {
      return { success: false, error: result.error, description: result.error_description };
    }
    
    if (result.status === 'expired') {
      return { success: false, error: 'expired', description: 'Session expired' };
    }
    
    // Still pending, wait before next poll
    await new Promise(resolve => setTimeout(resolve, 2000)); // 2 second interval
  }
  
  return { success: false, error: 'timeout', description: 'Polling timeout exceeded' };
}

// Usage
const result = await pollOAuthStatus(session_id, API_KEY);
if (result.success) {
  console.log(`Account connected: ${result.account.email}`);
} else {
  console.error(`OAuth failed: ${result.description}`);
}

Complete Example

Node.js Example

const fetch = require('node-fetch');

const API_KEY = 'your-api-key';
const API_BASE = 'https://api.instantly.ai/api/v2';

async function connectGoogleAccount() {
  // Step 1: Initialize OAuth session
  console.log('Initializing OAuth session...');
  
  const initResponse = await fetch(`${API_BASE}/oauth/google/init`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${API_KEY}`,
      'Content-Type': 'application/json'
    }
  });
  
  const { session_id, auth_url, expires_at } = await initResponse.json();
  
  console.log(`Session created: ${session_id}`);
  console.log(`Expires at: ${expires_at}`);
  console.log(`\nPlease open this URL in your browser to authenticate:\n${auth_url}\n`);
  
  // Step 2: Poll for completion
  console.log('Waiting for authentication...');
  
  const pollInterval = 2000; // 2 seconds
  const maxWaitTime = 10 * 60 * 1000; // 10 minutes
  const startTime = Date.now();
  
  while (Date.now() - startTime < maxWaitTime) {
    const statusResponse = await fetch(
      `${API_BASE}/oauth/session/status/${session_id}`,
      {
        headers: { 'Authorization': `Bearer ${API_KEY}` }
      }
    );
    
    const result = await statusResponse.json();
    
    switch (result.status) {
      case 'success':
        console.log('\n✓ Account connected successfully!');
        console.log(`  Email: ${result.email}`);
        console.log(`  Name: ${result.name}`);
        console.log(`  Account ID: ${result.account_id}`);
        return result;
        
      case 'error':
        console.error(`\n✗ OAuth failed: ${result.error}`);
        console.error(`  ${result.error_description}`);
        throw new Error(result.error_description);
        
      case 'expired':
        console.error('\n✗ Session expired');
        throw new Error('OAuth session expired');
        
      case 'pending':
        process.stdout.write('.');
        break;
    }
    
    await new Promise(resolve => setTimeout(resolve, pollInterval));
  }
  
  throw new Error('Polling timeout exceeded');
}

// Run
connectGoogleAccount()
  .then(() => process.exit(0))
  .catch(err => {
    console.error(err.message);
    process.exit(1);
  });

Python Example

import requests
import time

API_KEY = 'your-api-key'
API_BASE = 'https://api.instantly.ai/api/v2'

def connect_google_account():
    headers = {
        'Authorization': f'Bearer {API_KEY}',
        'Content-Type': 'application/json'
    }
    
    # Step 1: Initialize OAuth session
    print('Initializing OAuth session...')
    
    init_response = requests.post(
        f'{API_BASE}/oauth/google/init',
        headers=headers
    )
    init_data = init_response.json()
    
    session_id = init_data['session_id']
    auth_url = init_data['auth_url']
    expires_at = init_data['expires_at']
    
    print(f'Session created: {session_id}')
    print(f'Expires at: {expires_at}')
    print(f'\nPlease open this URL in your browser to authenticate:\n{auth_url}\n')
    
    # Step 2: Poll for completion
    print('Waiting for authentication...', end='', flush=True)
    
    poll_interval = 2  # seconds
    max_attempts = 300  # 10 minutes at 2 second intervals
    
    for _ in range(max_attempts):
        status_response = requests.get(
            f'{API_BASE}/oauth/session/status/{session_id}',
            headers=headers
        )
        result = status_response.json()
        
        if result['status'] == 'success':
            print('\n\n✓ Account connected successfully!')
            print(f"  Email: {result['email']}")
            print(f"  Name: {result['name']}")
            print(f"  Account ID: {result['account_id']}")
            return result
            
        elif result['status'] == 'error':
            print(f"\n\n✗ OAuth failed: {result['error']}")
            print(f"  {result.get('error_description', '')}")
            raise Exception(result.get('error_description', result['error']))
            
        elif result['status'] == 'expired':
            print('\n\n✗ Session expired')
            raise Exception('OAuth session expired')
            
        print('.', end='', flush=True)
        time.sleep(poll_interval)
    
    raise Exception('Polling timeout exceeded')

if __name__ == '__main__':
    connect_google_account()

Error Handling

Common Errors

Error CodeDescriptionResolution
account_existsAccount already connected to another workspaceThe Google account is already connected to a different Instantly workspace
server_errorInternal server errorRetry the OAuth flow
expiredSession expiredStart a new OAuth flow (sessions expire after 10 minutes)

HTTP Status Codes

CodeDescription
200Success
401Unauthorized - Invalid or missing API key
404Session not found or belongs to different workspace
429Rate limit exceeded

Security Considerations

  1. Session Binding: Sessions are tied to the workspace that created them. Only the same API key/workspace can poll for status.
  2. One-Time Use: Successful or error sessions are deleted after the first status retrieval.
  3. Short TTL: Sessions expire after 10 minutes if not completed.
  4. No Token Exposure: OAuth refresh tokens are never returned via the API - they're stored directly in the database.
  5. GSuite Only: Only Google Workspace (GSuite) accounts are supported. Personal Gmail accounts will be rejected.

Rate Limits

  • Init endpoint: Standard API rate limits apply
  • Status endpoint: Standard API rate limits apply
  • Recommended polling interval: 5 seconds

Troubleshooting

User sees "Non GSuite accounts are not allowed"

The OAuth flow only supports Google Workspace (GSuite) accounts. Personal Gmail accounts (@gmail.com) are not supported.

Session expires before user completes OAuth

Sessions have a 10-minute TTL. Ensure users complete the OAuth flow promptly. If needed, initialize a new session.

Status returns 404 "Session not found"

This can happen if:

  • The session ID is incorrect
  • The session was created by a different workspace/API key
  • The session has already been consumed (success/error status already retrieved)

Account shows as connected but status returns "error"

Check the error and error_description fields. Common issues:

  • Account already exists in another workspace
  • Account was blocked by admin
  • Account contains prohibited content