Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.callprep.app/llms.txt

Use this file to discover all available pages before exploring further.

Error categories

Client errors (4xx) — caused by invalid requests. Fix the request before retrying. Server errors (5xx) — caused by infrastructure issues. Safe to retry with backoff. Job failuresstatus: failed in research results. Usually safe to resubmit.

Production-ready error handler

class CallPrepClient {
  constructor(apiKey) {
    this.apiKey    = apiKey;
    this.baseUrl   = 'https://rpiqzfzokrwxavztrpmp.supabase.co/functions/v1';
  }

  async research(email, options = {}) {
    const res = await fetch(`${this.baseUrl}/research`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${this.apiKey}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ email, ...options }),
    });

    const data = await res.json();

    if (!res.ok) {
      throw new CallPrepError(data.error, res.status, data);
    }

    return data;
  }

  async pollStatus(researchId, { intervalMs = 5000, timeoutMs = 180000 } = {}) {
    const deadline = Date.now() + timeoutMs;

    while (Date.now() < deadline) {
      await sleep(intervalMs);

      const res  = await fetch(`${this.baseUrl}/research-status/${researchId}`, {
        headers: { 'Authorization': `Bearer ${this.apiKey}` },
      });
      const data = await res.json();

      if (data.status === 'completed') return data;
      if (data.status === 'failed') {
        throw new CallPrepError('job_failed', 200, data);
      }
    }

    throw new CallPrepError('polling_timeout', 0, { researchId });
  }
}

class CallPrepError extends Error {
  constructor(code, status, body) {
    super(`CallPrep error: ${code}`);
    this.code   = code;
    this.status = status;
    this.body   = body;
  }
}

const sleep = ms => new Promise(r => setTimeout(r, ms));

Handling specific error codes

try {
  const { research_id } = await client.research(email);
  const result          = await client.pollStatus(research_id);
  return result.data;

} catch (err) {
  if (!(err instanceof CallPrepError)) throw err;

  switch (err.code) {
    case 'credits_exhausted':
    case 'trial_limit_reached':
      // Notify user to upgrade — don't retry
      notifyUser('Credit limit reached. Please upgrade your plan.');
      break;

    case 'invalid_key':
      // Configuration issue — alert the developer
      alertDev('Invalid CallPrep API key');
      break;

    case 'invalid_email_format':
      // Validate email before calling API
      console.warn('Invalid email:', email);
      break;

    case 'job_failed':
      // Pipeline error — safe to retry once
      console.error('Research job failed:', err.body);
      break;

    case 'polling_timeout':
      // Job took too long — check research_id manually
      console.error('Polling timed out for:', err.body.researchId);
      break;

    default:
      // Unexpected error — log and alert
      console.error('Unexpected CallPrep error:', err);
  }
}

Retry strategy

ErrorRetry?Strategy
credits_exhaustedWait for next billing cycle
invalid_keyFix configuration
invalid_email_formatFix input data
job_failedRetry once after 5s
5xx errorsExponential backoff, max 3 retries
polling_timeoutResubmit with fresh research_id

Logging

Always log the research_id — it’s essential for debugging with support:
const { research_id } = await client.research(email);
logger.info('Research started', { research_id, email });

const result = await client.pollStatus(research_id);
logger.info('Research completed', { research_id, duration_ms: result._meta?.duration_ms });