Skip to main content

OAuth account API integration guide

This API allows third-party developers to programmatically connect Google and Microsoft 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 receive 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 the 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.
All code examples below use Google endpoints. To use Microsoft instead, 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
This endpoint has special rate limits to comply with upstream provider limits. See Rate limits for details.
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 the 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. You can do this 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 examples

Node.js

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('\nAccount 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(`\nOAuth failed: ${result.error}`);
        console.error(`  ${result.error_description}`);
        throw new Error(result.error_description);

      case 'expired':
        console.error('\nSession 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

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\nAccount 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\nOAuth failed: {result['error']}")
            print(f"  {result.get('error_description', '')}")
            raise Exception(result.get('error_description', result['error']))

        elif result['status'] == 'expired':
            print('\n\nSession 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

CodeEndpointDescription
200AllSuccess
401AllUnauthorized — invalid or missing API key
404StatusSession not found or belongs to a different workspace
429AllRate limit exceeded — init endpoints have additional stricter limits; all endpoints share the standard API rate limit
503Init onlyUpstream provider temporarily unavailable — retry after a short delay

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 are stored directly in the database.
  5. GSuite only: Only Google Workspace (GSuite) accounts are supported. Personal Gmail accounts will be rejected.

Rate limits

The OAuth init endpoints (/oauth/google/init and /oauth/microsoft/init) have stricter rate limits than the standard API rate limit. These exist to stay compliant with upstream provider (Google, Microsoft) rate limits on OAuth authorization requests.
  • Init endpoint (per workspace): 75 requests per minute. Exceeding returns 429.
  • Init endpoint (per IP): 150 requests per minute. Exceeding returns 429.
  • Escalating IP blocks: Persistent abuse from the same IP triggers progressively longer blocks (30 minutes → 2 hours → 24 hours).
When an IP is blocked, the 429 response includes a Retry-After header with the remaining block duration in seconds. Use this value to schedule your next retry.
  • Circuit breaker: If the upstream provider (Google/Microsoft) is temporarily rate-limiting Instantly, init requests return 503 Service Unavailable. This resolves automatically — retry after a short delay.
  • Provider isolation: Rate limits, IP blocks, and circuit breakers are tracked per provider. A Microsoft outage does not affect Google init requests, and vice versa.
  • 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 the 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.