Create screening interviews programmatically.
Create a new interview invitation for a candidate.
POST /api/v1/invitations
This endpoint:
Headers:
Authorization: Bearer rz_live_...
Content-Type: application/json
Idempotency-Key: unique-key-12345 (optional)
Body:
{
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"candidate_name": "Jane Smith",
"candidate_cv": {
"summary": "Experienced software engineer with 5+ years in React and TypeScript",
"workExperiences": [
{
"company": "Tech Corp",
"title": "Senior Developer",
"startDate": "2020-01",
"isCurrent": true,
"description": "Lead frontend development team"
}
],
"skills": [
{ "name": "React", "category": "technical" },
{ "name": "TypeScript", "category": "technical" }
]
},
"question_config": {
"excluded_question_ids": [
"770g0622-g41d-63f6-c938-668877662222"
],
"custom_questions": [
{
"question_text": "Why do you want to work at our company?",
"question_type": "behavioral",
"time_limit_seconds": 120,
"order_index": 10
}
],
"question_order": [
"880h1733-h52e-74g7-d049-779988773333",
"custom_0",
"990i2844-i63f-85h8-e150-880099884444"
]
},
"expires_in_days": 7
}
Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
job_id | string (UUID) | Yes | ID of the job to interview for |
candidate_name | string | Yes | Candidate's full name |
candidate_cv | object | No | Candidate CV data for personalized interviews (see below) |
question_config | object | No | Question customization |
question_config.excluded_question_ids | array | No | UUIDs of job questions to exclude |
question_config.custom_questions | array | No | Custom questions to add |
question_config.question_order | array | No | Array of question IDs defining display order |
custom_questions[].question_text | string | Yes | The question to ask |
custom_questions[].question_type | string | Yes | Type: behavioral, technical, situational, experience, role_specific |
custom_questions[].time_limit_seconds | integer | No | Time limit (default: 180) |
custom_questions[].order_index | integer | No | Order index for the question |
expires_in_days | integer | No | Number of days until link expires (1-90, default varies) |
locale_id | string | No | Language code (e.g., "en", "es", "fr") |
voice_id | string | No | ElevenLabs voice ID to use (see Available Voices below) |
201 Created:
{
"id": "660f9511-f30c-52e5-b827-557766551111",
"interview_url": "https://interview.round0.io/interview/abc123def456...",
"token": "abc123def456...",
"status": "invited",
"candidate_name": "Jane Smith",
"job": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Senior Frontend Developer"
},
"estimated_credits": 2,
"estimated_duration_seconds": 1260,
"expires_at": "2024-01-22T23:59:59Z",
"created_at": "2024-01-15T10:30:00Z"
}
Response fields:
id: Interview UUIDinterview_url: Complete URL to share with candidatetoken: Unique interview token (part of URL)status: Always "invited" for new interviewscandidate_name: Candidate's name from requestestimated_credits: Credits reserved for this interviewestimated_duration_seconds: Total time for all questionsexpires_at: When the interview link expirescreated_at: ISO 8601 timestampBasic invitation:
curl -X POST https://rz-app-omega.vercel.app/api/v1/invitations \
-H "Authorization: Bearer rz_live_..." \
-H "Content-Type: application/json" \
-d '{
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"candidate_name": "Jane Smith"
}'
With custom questions:
curl -X POST https://rz-app-omega.vercel.app/api/v1/invitations \
-H "Authorization: Bearer rz_live_..." \
-H "Content-Type: application/json" \
-d '{
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"candidate_name": "Jane Smith",
"question_config": {
"custom_questions": [
{
"question_text": "What interests you most about this role?",
"question_type": "behavioral",
"time_limit_seconds": 120
}
]
}
}'
With idempotency key (safe retries):
curl -X POST https://rz-app-omega.vercel.app/api/v1/invitations \
-H "Authorization: Bearer rz_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 880h1733-h52e-74g7-d049-779988773333" \
-d '{
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"candidate_name": "Jane Smith"
}'
JavaScript example:
async function createInterview(jobId, candidateName) {
const response = await fetch('https://rz-app-omega.vercel.app/api/v1/invitations', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.ROUNDZERO_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
job_id: jobId,
candidate_name: candidateName
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error.message);
}
const interview = await response.json();
return interview;
}
400 Bad Request - Validation error:
{
"error": {
"type": "validation_error",
"message": "Validation failed",
"details": {
"job_id": "Invalid job ID",
"candidate.name": "Name is required"
}
}
}
403 Forbidden - Insufficient credits:
{
"error": {
"type": "insufficient_credits",
"message": "Insufficient available credits",
"details": {
"required_credits": 2,
"available_credits": 0,
"total_balance": 5,
"reserved_credits": 5
}
}
}
404 Not Found - Job doesn't exist:
{
"error": {
"type": "resource_not_found",
"message": "Job not found"
}
}
429 Too Many Requests - Rate limit exceeded:
{
"error": {
"type": "rate_limit_exceeded",
"message": "Rate limit exceeded. Please retry after 30 seconds."
}
}
The API accepts the candidate's name as a simple string field.
How it works:
candidate_name in the requestExample:
// Create interview with candidate name
const interview = await createInvitation({
job_id: "job-123",
candidate_name: "John Doe"
});
// Interview created with candidate_name: "John Doe"
You can optionally include candidate CV data for personalized interview experiences. When CV data is provided, the AI interviewer can:
CV Data Structure:
{
"candidate_cv": {
"summary": "Brief professional summary",
"workExperiences": [
{
"company": "Company Name",
"title": "Job Title",
"startDate": "2020-01",
"endDate": "2024-01",
"isCurrent": false,
"description": "Role description and achievements"
}
],
"education": [
{
"institution": "University Name",
"degree": "Bachelor",
"field": "Computer Science",
"endDate": "2019"
}
],
"skills": [
{ "name": "JavaScript", "category": "technical", "proficiency": "expert" },
{ "name": "Leadership", "category": "soft" }
],
"certifications": [
{
"name": "AWS Solutions Architect",
"issuer": "Amazon Web Services",
"issueDate": "2023-06"
}
],
"linkedinUrl": "https://linkedin.com/in/candidate",
"portfolioUrl": "https://candidate.dev"
}
}
Field Reference:
| Field | Type | Description |
|---|---|---|
summary | string | Professional summary or bio |
workExperiences | array | Employment history |
workExperiences[].company | string | Company name |
workExperiences[].title | string | Job title |
workExperiences[].startDate | string | Start date (YYYY-MM format) |
workExperiences[].endDate | string | End date (YYYY-MM format) |
workExperiences[].isCurrent | boolean | Whether currently employed |
workExperiences[].description | string | Role description |
education | array | Educational background |
education[].institution | string | School/university name |
education[].degree | string | Degree type |
education[].field | string | Field of study |
skills | array | Technical and soft skills |
skills[].name | string | Skill name |
skills[].category | string | "technical", "soft", or "language" |
skills[].proficiency | string | "beginner", "intermediate", "advanced", or "expert" |
certifications | array | Professional certifications |
linkedinUrl | string | LinkedIn profile URL |
portfolioUrl | string | Portfolio or personal website |
For detailed CV data schemas and standalone candidate management, see the Candidates API.
Exclude specific job questions by providing their IDs:
{
"question_config": {
"excluded_question_ids": [
"770g0622-g41d-63f6-c938-668877662222",
"880h1733-h52e-74g7-d049-779988773333"
]
}
}
To get question IDs:
GET /api/v1/jobs/:id to see all questionsid field for each questionexcluded_question_idsAdd your own questions to the interview:
{
"question_config": {
"custom_questions": [
{
"question_text": "Describe your experience with our specific tech stack.",
"question_type": "technical",
"time_limit_seconds": 240
},
{
"question_text": "Why do you want to work at our company?",
"question_type": "behavioral",
"time_limit_seconds": 120
}
]
}
}
Question types:
behavioral: How candidates handle situationstechnical: Domain knowledge and skillssituational: Problem-solving scenariosexperience: Past work experiencerole_specific: Unique to the positionTime limits:
Control the order in which questions are asked:
{
"question_config": {
"question_order": [
"job-question-uuid-1",
"custom_0",
"job-question-uuid-2",
"custom_1"
]
}
}
How it works:
custom_0, custom_1, etc. for custom questions (based on their index in the custom_questions array)You can exclude questions, add custom questions, and set order all at once:
{
"question_config": {
"excluded_question_ids": ["q1-uuid", "q2-uuid"],
"custom_questions": [
{
"question_text": "Custom question here",
"question_type": "behavioral",
"time_limit_seconds": 180,
"order_index": 10
}
],
"question_order": ["q3-uuid", "custom_0", "q4-uuid"]
}
}
Set how many days until the interview link expires:
{
"expires_in_days": 7
}
Format: Integer (1-90 days)
Recommendations:
What happens on expiration:
Round Zero offers a selection of professional ElevenLabs voices for interviews. You can specify which voice to use via the voice_id parameter.
If no voice_id is specified, interviews will use Rachel (Professional Female) by default.
| Voice ID | Name | Description |
|---|---|---|
21m00Tcm4TlvDq8ikWAM | Rachel | Professional Female (Default) |
AZnzlk1XvdvUeBnXmlld | Domi | Confident Female |
EXAVITQu4vr4xnSDxMaL | Bella | Warm Female |
ErXwobaYiN019PkySvjV | Antoni | Well-Rounded Male |
MF3mGyEYCl7XYWbV9V6O | Elli | Emotional Female |
TxGEqnHWrfWFTfGW9XjX | Josh | Deep Male |
VR6AewLTigWG4xSOukaG | Arnold | Crisp Male |
pNInz6obpgDQGcFmaJgB | Adam | American Male |
yoZ06aMxZJJ28mfd3POQ | Sam | Raspy Male |
Include the voice_id in your invitation request:
{
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"candidate_name": "Jane Smith",
"voice_id": "TxGEqnHWrfWFTfGW9XjX"
}
Example with Josh (Deep Male):
curl -X POST https://rz-app-omega.vercel.app/api/v1/invitations \
-H "Authorization: Bearer rz_live_..." \
-H "Content-Type: application/json" \
-d '{
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"candidate_name": "Jane Smith",
"voice_id": "TxGEqnHWrfWFTfGW9XjX"
}'
Automatically create interviews when candidates reach a stage:
// When candidate moves to "Screening" stage in your ATS
async function onCandidateScreening(atsCandidate) {
// Get mapped ROUND0 job
const mapping = await db.jobMappings.findOne({
ats_job_id: atsCandidate.job_id
});
// Create interview
const interview = await fetch('https://rz-app-omega.vercel.app/api/v1/invitations', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
'Idempotency-Key': `ats-${atsCandidate.id}` // Prevent duplicates
},
body: JSON.stringify({
job_id: mapping.round0_job_id,
candidate_name: atsCandidate.name,
expires_in_days: 7
})
}).then(r => r.json());
// Update ATS with interview link
await ats.updateCandidate(atsCandidate.id, {
screening_url: interview.interview_url
});
// Send email via your system
await sendEmail({
to: atsCandidate.email,
template: 'screening_invitation',
data: {
candidate_name: atsCandidate.name,
job_title: mapping.job_title,
interview_url: interview.interview_url,
expires_at: interview.expires_at
}
});
}
Create multiple interviews efficiently:
async function createBulkInterviews(jobId, candidates) {
const results = [];
// Process in batches to respect rate limits
const batchSize = 10;
for (let i = 0; i < candidates.length; i += batchSize) {
const batch = candidates.slice(i, i + batchSize);
const promises = batch.map(candidate =>
fetch('https://rz-app-omega.vercel.app/api/v1/invitations', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
job_id: jobId,
candidate_name: candidate.name
})
}).then(r => r.json())
);
const batchResults = await Promise.all(promises);
results.push(...batchResults);
// Small delay between batches
if (i + batchSize < candidates.length) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
return results;
}
Always use idempotency keys for reliable integrations:
const idempotencyKey = `${sourceSystem}-${candidateId}-${jobId}`;
const interview = await createInterview({
headers: { 'Idempotency-Key': idempotencyKey },
// ... rest of request
});
Handle all possible error scenarios:
async function createInterviewSafely(jobId, candidate) {
try {
const response = await fetch('/api/v1/invitations', {
method: 'POST',
headers: { /* ... */ },
body: JSON.stringify({ job_id: jobId, candidate })
});
if (!response.ok) {
const error = await response.json();
if (error.error.type === 'insufficient_credits') {
await notifyAdmin('Need to purchase more credits');
throw new Error('Insufficient credits');
}
if (error.error.type === 'rate_limit_exceeded') {
// Retry with exponential backoff
await sleep(5000);
return createInterviewSafely(jobId, candidate);
}
throw new Error(error.error.message);
}
return await response.json();
} catch (error) {
console.error('Failed to create interview:', error);
throw error;
}
}
Check credits before bulk operations:
// Check credit balance first
const balance = await fetch('/api/v1/credits/balance', {
headers: { 'Authorization': `Bearer ${apiKey}` }
}).then(r => r.json());
const requiredCredits = candidates.length * 2; // Assuming 2 credits per interview
if (balance.available_credits < requiredCredits) {
throw new Error(`Need ${requiredCredits} credits, only ${balance.available_credits} available`);
}
// Proceed with bulk creation