Skip to content

Troubleshooting

This guide covers common issues when integrating with the Contio Partner API and how to resolve them.

OAuth Authentication

invalid_grant - Authorization Code Failed

Symptom: Token exchange returns invalid_grant error with HTTP 400.

Causes:

  • Authorization code expired (codes must be exchanged within 5 minutes)
  • Code was already used (codes are single-use)
  • Refresh token is expired or has been revoked
  • Authorization code was issued to a different client

Solution:

try {
  const tokens = await oauth.exchangeCodeForToken(code);
} catch (error) {
  if (error.code === 'invalid_grant') {
    // Restart the OAuth flow
    return res.redirect('/connect');
  }
}

invalid_request - Request Parameter Issues

Symptom: Token exchange returns invalid_request error with HTTP 400.

Causes:

  • redirect_uri doesn't match the one used in the authorization request
  • Missing required parameter

Solution:

Ensure redirect_uri matches exactly (including trailing slashes and query parameters):

// In authorization request
const authUrl = `${authorizationUrl}?redirect_uri=${encodeURIComponent('https://app.example.com/callback')}`;

// In token exchange - must match exactly
const tokens = await oauth.exchangeCodeForToken(code);
// SDK uses redirectUri from config, ensure it matches

session_expired - OTP Session Timeout

Symptom: Consent check or authorization fails with session_expired.

Cause: The authenticated session established during OTP verification has a 30-minute TTL. If the user takes too long between OTP verification and granting consent, the session expires.

Solution: Redirect the user back to the login page to restart the flow:

if (error.code === 'session_expired') {
  return res.redirect('/connect?error=session_expired');
}

invalid_code - OTP Verification Failed

Symptom: /auth/verify returns invalid_code error.

Causes:

  • User entered the wrong OTP code
  • OTP code has expired (valid for 3 minutes)
  • Code was already used

Solution: Prompt the user to request a new code:

if (error.code === 'invalid_code') {
  // Show error message and option to resend code
  return res.render('otp', { error: 'Invalid or expired code. Please try again.' });
}

invalid_client - Bad Credentials

Symptom: Any API call returns invalid_client.

Causes:

  • Incorrect client_id or client_secret
  • Partner app has been deactivated

Solution:

  1. Verify credentials in your environment configuration
  2. Check that credentials match those in the Contio Partner Dashboard
  3. Contact support if your partner app may have been deactivated

insufficient_scope - Access Token Has No Scopes

Symptom: API calls return insufficient_scope or 403 error. Token introspection shows empty or missing scope field.

Causes:

  1. User didn't complete consent - The OAuth flow was interrupted before the user granted permission on the consent screen
  2. Scopes not requested - The authorization URL didn't include the scope parameter
  3. Consent was revoked - User previously granted consent but later revoked it

Diagnosis: Use token introspection to check what scopes the token has:

const tokenInfo = await oauth.introspectToken(accessToken);
console.log('Token scopes:', tokenInfo.scope);
// If empty or undefined, consent was not granted
curl -X POST https://api.contio.ai/oauth2/introspect \
  -H "Authorization: Basic $(echo -n 'CLIENT_ID:CLIENT_SECRET' | base64)" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "token=YOUR_ACCESS_TOKEN"

Response (token with scopes):

{
  "active": true,
  "scope": "openid profile meetings:read action-items:read",
  "client_id": "your-client-id",
  "exp": 1704672000
}

Response (token without scopes - consent not completed):

{
  "active": true,
  "scope": "",
  "client_id": "your-client-id"
}

Solution:

The user must complete the full OAuth flow including the consent step:

  1. Redirect user to /oauth2/authorize with required scopes
  2. User enters email and verifies via OTP
  3. User sees consent screen and clicks "Allow" ← This step grants scopes
  4. User is redirected back with authorization code
  5. Exchange code for tokens
// Ensure scopes are requested in authorization URL
const authUrl = oauth.getAuthorizationUrl(state, [
  'openid',
  'profile',
  'meetings:read',
  'action-items:read'
]);
// Redirect user to authUrl - they MUST complete consent

Common Mistake

If you're testing with automated flows that skip the consent UI, tokens will have no scopes. The consent screen cannot be bypassed in production.


Token Has Fewer Scopes Than Requested

Symptom: Token works but is missing some requested scopes.

Causes:

  1. User only granted partial consent (declined some permissions)
  2. Your partner app isn't approved for certain scopes
  3. Scopes were misspelled in the authorization request

Solution:

Check the token's actual scopes and handle gracefully:

const tokenInfo = await oauth.introspectToken(accessToken);
const grantedScopes = (tokenInfo.scope || '').split(' ');

if (!grantedScopes.includes('meetings:read')) {
  // User didn't grant meetings access - show appropriate UI
  return res.render('limited-access', {
    message: 'Meeting access was not granted'
  });
}
# Introspect the token to see granted scopes
curl -X POST https://api.contio.ai/oauth2/introspect \
  -H "Authorization: Basic $(echo -n 'CLIENT_ID:CLIENT_SECRET' | base64)" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "token=YOUR_ACCESS_TOKEN"

Then check the scope field in the response against what you requested.


405 Method Not Allowed

Symptom: API returns 405 Method Not Allowed.

Cause: Using wrong HTTP method for the endpoint.

Common mistakes:

Wrong Correct
GET /oauth2/token POST /oauth2/token
GET /auth/initiate POST /auth/initiate
GET /auth/verify POST /auth/verify

Authentication Method Confusion

The Partner API uses three different authentication methods. Using the wrong method is a common source of errors.

Quick Reference: Which Auth Method to Use

Endpoint Auth Method Headers
POST /oauth2/token HTTP Basic Authorization: Basic base64(client_id:client_secret)
/v1/partner/user/* Bearer Token Authorization: Bearer <access_token>
/v1/partner/admin/* API Key X-API-Key + X-Client-ID

Using Bearer Token on Token Endpoint

Symptom: POST /oauth2/token returns invalid_client or 401.

Cause: Using Authorization: Bearer instead of Authorization: Basic for token exchange.

Wrong:

curl -X POST https://api.contio.ai/oauth2/token \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -d "grant_type=authorization_code&code=AUTH_CODE"

Correct:

curl -X POST https://api.contio.ai/oauth2/token \
  -H "Authorization: Basic $(echo -n 'CLIENT_ID:CLIENT_SECRET' | base64)" \
  -d "grant_type=authorization_code&code=AUTH_CODE"


Using API Key on User Endpoints

Symptom: /v1/partner/user/* returns 401 even with valid API key.

Cause: User endpoints require OAuth Bearer tokens, not API keys. API keys are only for admin endpoints.

Wrong:

curl https://api.contio.ai/v1/partner/user/meetings \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-Client-ID: YOUR_CLIENT_ID"

Correct:

curl https://api.contio.ai/v1/partner/user/meetings \
  -H "Authorization: Bearer USER_ACCESS_TOKEN"


Using Bearer Token on Admin Endpoints

Symptom: /v1/partner/admin/* returns 401 with valid access token.

Cause: Admin endpoints require API key authentication, not OAuth tokens.

Wrong:

curl https://api.contio.ai/v1/partner/admin/stats \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Correct:

curl https://api.contio.ai/v1/partner/admin/stats \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-Client-ID: YOUR_CLIENT_ID"


Missing X-Client-ID Header

Symptom: Admin endpoint returns missing_client_id error.

Cause: Admin endpoints require both X-API-Key and X-Client-ID headers.

Solution: Always include both headers for admin endpoints:

curl https://api.contio.ai/v1/partner/admin/workflows \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-Client-ID: YOUR_CLIENT_ID"

Basic Auth Encoding Issues

Symptom: Token endpoint returns invalid_client with correct credentials.

Causes:

  • Credentials not Base64 encoded
  • Missing colon separator between client_id and client_secret
  • Extra whitespace or newlines in encoded string

Solution: Verify your Base64 encoding:

# Correct encoding (no newline with -n flag)
echo -n "client_id:client_secret" | base64

# Common mistake: missing -n adds newline
echo "client_id:client_secret" | base64  # Wrong!

API Requests

rate_limit_exceeded - Too Many Requests

Symptom: API returns HTTP 429 with rate_limit_exceeded.

Cause: You've exceeded the rate limit for your endpoint type.

Endpoint Type Rate Limit
User API 100 requests/minute
Admin API 50 requests/minute
OAuth 20 requests/minute

Solution: Implement exponential backoff:

async function apiCallWithRetry(fn: () => Promise<any>, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      if (error.code === 'rate_limit_exceeded' && i < maxRetries - 1) {
        const resetTime = error.response?.headers?.['x-ratelimit-reset'];
        const waitMs = resetTime
          ? (parseInt(resetTime) * 1000) - Date.now()
          : Math.pow(2, i) * 1000;
        await new Promise(r => setTimeout(r, Math.max(waitMs, 1000)));
        continue;
      }
      throw error;
    }
  }
}

network_error - No Response

Symptom: SDK throws network_error with "No response received from server".

Causes:

  • Network connectivity issues
  • Request timeout
  • Firewall blocking requests

Solution:

  1. Check network connectivity to api.contio.ai
  2. Increase timeout if processing large requests
  3. Verify firewall allows outbound HTTPS (port 443)

Webhook Issues

Signature Verification Failed

Symptom: verifyWebhookSignature() returns false.

Causes:

  • Using wrong webhook secret
  • Request body was modified (middleware parsing)
  • Signature header missing or malformed

Solution: Use raw body for verification:

// ❌ Wrong - body may be modified
app.post('/webhooks', express.json(), (req, res) => {
  const isValid = verifyWebhookSignature(req.body, signature, secret);
});

// ✅ Correct - use raw body
app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
  const isValid = verifyWebhookSignature(req.body, signature, secret);
});

Missing Signature Header

Symptom: X-Contio-Signature header is empty or missing.

Cause: Request isn't coming from Contio (possibly a test request or attack).

Solution: Always verify the signature exists before processing:

const signature = req.headers['x-contio-signature'];
if (!signature) {
  return res.status(400).json({ error: 'Missing signature' });
}

Using Request ID for Support

All error responses include a request_id field for debugging:

{
  "error": "invalid_grant",
  "error_description": "Authorization code has expired",
  "request_id": "req_abc123xyz"
}

When contacting support, always include the request_id to help us trace the issue.


Getting Help