round0
API Reference

Interviews API

Retrieve interview data, transcripts, and recordings

Interviews API

Retrieve interview results and transcripts.

Get Interview

Retrieve complete interview results including transcripts.

GET /api/v1/interviews/:id
This endpoint only returns data for completed interviews. Pending or in-progress interviews will return limited information.

Request

Headers:

Authorization: Bearer rz_live_...

Path Parameters:

  • id: Interview UUID

Response

200 OK (Completed interview):

{
  "id": "660f9511-f30c-52e5-b827-557766551111",
  "status": "completed",
  "job": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "title": "Senior Frontend Developer",
    "department": "Engineering"
  },
  "candidate": {
    "id": "770g0622-g41d-63f6-c938-668877662222",
    "name": "Jane Smith",
    "email": "jane@example.com"
  },
  "started_at": "2024-01-16T14:30:00Z",
  "completed_at": "2024-01-16T14:52:00Z",
  "responses": [
    {
      "question_id": "880h1733-h52e-74g7-d049-779988773333",
      "question_text": "Describe your experience with React hooks and when you would use them.",
      "question_type": "technical",
      "transcript": "I've been using React hooks extensively for the past three years. Hooks like useState and useEffect are fundamental to my development workflow. I particularly appreciate how they enable functional components to manage state and side effects without the complexity of class components. For instance, I recently used useReducer for complex state logic in a form validation system, and useMemo to optimize expensive calculations in a data visualization dashboard. The custom hooks I've created have helped our team maintain consistent patterns across the codebase.",
      "duration_seconds": 145,
      "answered_at": "2024-01-16T14:33:25Z"
    },
    {
      "question_id": "990i2844-i63f-85h8-e150-880099884444",
      "question_text": "Tell me about a challenging bug you fixed recently.",
      "question_type": "experience",
      "transcript": "Last month, we had an intermittent race condition that only appeared in production...",
      "duration_seconds": 167,
      "answered_at": "2024-01-16T14:38:12Z"
    }
  ],
  "created_at": "2024-01-15T10:30:00Z"
}

Response fields:

  • id: Interview UUID
  • status: Interview status (completed, in_progress, invited, expired)
  • job: Job information
  • candidate: Candidate information
  • started_at: When candidate began interview (ISO 8601)
  • completed_at: When interview finished (ISO 8601)
  • responses[]: Array of question responses
  • responses[].question_id: Question UUID
  • responses[].question_text: The question that was asked
  • responses[].question_type: Question category
  • responses[].transcript: Full text transcription of response
  • responses[].audio_url: URL to audio recording (temporary, expires in 24h)
  • responses[].duration_seconds: Length of response
  • responses[].answered_at: Timestamp of response

200 OK (Non-completed interview):

{
  "id": "660f9511-f30c-52e5-b827-557766551111",
  "status": "invited",
  "job": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "title": "Senior Frontend Developer"
  },
  "candidate": {
    "id": "770g0622-g41d-63f6-c938-668877662222",
    "name": "Jane Smith",
    "email": "jane@example.com"
  },
  "created_at": "2024-01-15T10:30:00Z",
  "expires_at": "2024-01-22T23:59:59Z"
}

Example

curl https://rz-app-omega.vercel.app/api/v1/interviews/660f9511-f30c-52e5-b827-557766551111 \
  -H "Authorization: Bearer rz_live_..."
async function getInterviewResults(interviewId) {
  const response = await fetch(
    `https://rz-app-omega.vercel.app/api/v1/interviews/${interviewId}`,
    {
      headers: {
        'Authorization': `Bearer ${process.env.ROUNDZERO_API_KEY}`
      }
    }
  );

  if (!response.ok) {
    throw new Error('Failed to fetch interview');
  }

  const interview = await response.json();

  // Only completed interviews have responses
  if (interview.status === 'completed') {
    interview.responses.forEach(response => {
      console.log(`Q: ${response.question_text}`);
      console.log(`A: ${response.transcript}\n`);
    });
  }

  return interview;
}

Errors

404 Not Found - Interview doesn't exist:

{
  "error": {
    "type": "resource_not_found",
    "message": "Interview not found"
  }
}

Get Interview Questions

Retrieve the questions that were asked in an interview.

GET /api/v1/interviews/:id/questions

This endpoint returns the questions regardless of interview status.

Request

Headers:

Authorization: Bearer rz_live_...

Path Parameters:

  • id: Interview UUID

Response

200 OK:

{
  "interview_id": "660f9511-f30c-52e5-b827-557766551111",
  "questions": [
    {
      "id": "880h1733-h52e-74g7-d049-779988773333",
      "question_text": "Describe your experience with React hooks and when you would use them.",
      "question_type": "technical",
      "time_limit_seconds": 180,
      "order": 0
    },
    {
      "id": "990i2844-i63f-85h8-e150-880099884444",
      "question_text": "Tell me about a challenging bug you fixed recently.",
      "question_type": "experience",
      "time_limit_seconds": 180,
      "order": 1
    },
    {
      "id": "custom-aa0j3955-j74g-96i9-f261-991100995555",
      "question_text": "Why do you want to work at our company?",
      "question_type": "behavioral",
      "time_limit_seconds": 120,
      "order": 10,
      "is_custom": true
    }
  ],
  "total_questions": 11,
  "estimated_duration_seconds": 1260
}

Response fields:

  • interview_id: Interview UUID
  • questions[]: Array of questions
  • questions[].is_custom: True if added via API (not from job template)
  • total_questions: Count of questions
  • estimated_duration_seconds: Total time for all questions

Example

curl https://rz-app-omega.vercel.app/api/v1/interviews/660f9511-f30c-52e5-b827-557766551111/questions \
  -H "Authorization: Bearer rz_live_..."

Understanding Interview Status

Status Lifecycle

invited

  • Interview created but candidate hasn't started
  • No responses available
  • Link is still valid (if not expired)
  • Credits reserved but not consumed

in_progress

  • Candidate has started the interview
  • Partial responses may be available
  • Credits have been consumed
  • Not yet completed

completed

  • All questions answered
  • Full transcripts available
  • Ready for review
  • Cannot be restarted

expired

  • Link expired before candidate started
  • No responses
  • Credits released back to available balance
  • Cannot be started

Checking Status

Before fetching results, check status:

async function waitForCompletion(interviewId, maxWaitMs = 3600000) {
  const startTime = Date.now();

  while (Date.now() - startTime < maxWaitMs) {
    const interview = await fetch(
      `https://rz-app-omega.vercel.app/api/v1/interviews/${interviewId}`,
      { headers: { 'Authorization': `Bearer ${apiKey}` } }
    ).then(r => r.json());

    if (interview.status === 'completed') {
      return interview;
    }

    if (interview.status === 'expired') {
      throw new Error(`Interview ${interview.status}`);
    }

    // Wait before checking again
    await new Promise(resolve => setTimeout(resolve, 60000)); // 1 minute
  }

  throw new Error('Timeout waiting for interview completion');
}
Best practice: Use webhooks instead of polling for interview completion. Webhooks provide real-time notifications and are more efficient.

Data Privacy & Retention

Audio Recordings

  • Audio URLs are temporary and expire after 24 hours
  • Download and store locally if long-term access needed
  • URLs are signed and cannot be shared publicly

Transcripts

  • Transcripts are permanently available via API
  • No expiration on transcript data
  • Access controlled by API authentication

Personal Data

The API returns only what's necessary:

  • No raw audio files in responses (only temporary URLs)
  • Transcripts contain candidate responses
  • Follow your organization's data retention policies

Use Cases

ATS Sync

Sync interview results back to your ATS:

// Called when webhook receives "interview_ended" event
async function syncInterviewToATS(interviewId) {
  // Get full interview results
  const interview = await fetch(
    `https://rz-app-omega.vercel.app/api/v1/interviews/${interviewId}`,
    { headers: { 'Authorization': `Bearer ${apiKey}` } }
  ).then(r => r.json());

  // Update candidate in ATS
  await ats.updateCandidate({
    id: interview.candidate.email,
    screening_status: 'completed',
    screening_completed_at: interview.completed_at,
    screening_notes: formatTranscripts(interview.responses)
  });

  // Download and attach audio files
  for (const response of interview.responses) {
    const audioBlob = await fetch(response.audio_url).then(r => r.blob());
    await ats.attachFile({
      candidate_id: interview.candidate.email,
      filename: `interview_q${response.question_id}.mp3`,
      file: audioBlob
    });
  }
}

function formatTranscripts(responses) {
  return responses.map((r, i) =>
    `Q${i + 1}: ${r.question_text}\nA: ${r.transcript}\n`
  ).join('\n---\n\n');
}

Automated Review

Analyze transcripts programmatically:

async function analyzeInterview(interviewId) {
  const interview = await fetch(
    `https://rz-app-omega.vercel.app/api/v1/interviews/${interviewId}`,
    { headers: { 'Authorization': `Bearer ${apiKey}` } }
  ).then(r => r.json());

  const analysis = {
    candidate: interview.candidate.name,
    completedAt: interview.completed_at,
    totalDuration: interview.responses.reduce((sum, r) => sum + r.duration_seconds, 0),
    averageResponseLength: interview.responses.reduce((sum, r) => sum + r.transcript.length, 0) / interview.responses.length,
    keywordMatches: analyzeKeywords(interview.responses)
  };

  return analysis;
}

function analyzeKeywords(responses) {
  const keywords = ['react', 'typescript', 'testing', 'agile', 'ci/cd'];
  const matches = {};

  keywords.forEach(keyword => {
    matches[keyword] = responses.filter(r =>
      r.transcript.toLowerCase().includes(keyword)
    ).length;
  });

  return matches;
}

Batch Export

Export multiple interview results:

async function exportInterviews(interviewIds) {
  const results = await Promise.all(
    interviewIds.map(id =>
      fetch(`https://rz-app-omega.vercel.app/api/v1/interviews/${id}`, {
        headers: { 'Authorization': `Bearer ${apiKey}` }
      }).then(r => r.json())
    )
  );

  // Convert to CSV
  const csv = convertToCSV(results);
  fs.writeFileSync('interview_export.csv', csv);

  return results;
}

Best Practices

When to Fetch Results

Don't poll - Use webhooks instead:

  • ✅ Set up webhook for interview_ended event
  • ✅ Fetch results when webhook fires
  • ❌ Don't poll every minute checking for completion

Caching

Cache interview results to reduce API calls:

  • Results don't change once interview is completed
  • Store locally after first fetch
  • Update only if interview is re-taken (rare)

Audio Storage

Download audio files if needed long-term:

async function archiveAudio(interview) {
  for (const response of interview.responses) {
    const audio = await fetch(response.audio_url).then(r => r.arrayBuffer());
    await storage.save(
      `interviews/${interview.id}/q${response.question_id}.mp3`,
      audio
    );
  }
}

Error Handling

Handle cases where interview isn't ready:

async function getResults(interviewId) {
  const interview = await fetch(
    `https://rz-app-omega.vercel.app/api/v1/interviews/${interviewId}`,
    { headers: { 'Authorization': `Bearer ${apiKey}` } }
  ).then(r => r.json());

  if (interview.status !== 'completed') {
    console.log(`Interview not ready: ${interview.status}`);
    return null;
  }

  return interview;
}

Next Steps