round0
API Reference

Invitations API

Create interview invitations for candidates via API

Invitations API

Create screening interviews programmatically.

Create Invitation

Create a new interview invitation for a candidate.

POST /api/v1/invitations

This endpoint:

  • Creates or finds a candidate by email
  • Reserves the required credits
  • Generates a unique interview link
  • Returns the interview URL to share with the candidate

Request

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:

FieldTypeRequiredDescription
job_idstring (UUID)YesID of the job to interview for
candidate_namestringYesCandidate's full name
candidate_cvobjectNoCandidate CV data for personalized interviews (see below)
question_configobjectNoQuestion customization
question_config.excluded_question_idsarrayNoUUIDs of job questions to exclude
question_config.custom_questionsarrayNoCustom questions to add
question_config.question_orderarrayNoArray of question IDs defining display order
custom_questions[].question_textstringYesThe question to ask
custom_questions[].question_typestringYesType: behavioral, technical, situational, experience, role_specific
custom_questions[].time_limit_secondsintegerNoTime limit (default: 180)
custom_questions[].order_indexintegerNoOrder index for the question
expires_in_daysintegerNoNumber of days until link expires (1-90, default varies)
locale_idstringNoLanguage code (e.g., "en", "es", "fr")
voice_idstringNoElevenLabs voice ID to use (see Available Voices below)

Response

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 UUID
  • interview_url: Complete URL to share with candidate
  • token: Unique interview token (part of URL)
  • status: Always "invited" for new interviews
  • candidate_name: Candidate's name from request
  • estimated_credits: Credits reserved for this interview
  • estimated_duration_seconds: Total time for all questions
  • expires_at: When the interview link expires
  • created_at: ISO 8601 timestamp

Example Requests

Basic 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;
}

Errors

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."
  }
}

Candidate Name

The API accepts the candidate's name as a simple string field.

How it works:

  1. You provide candidate_name in the request
  2. The interview is created with that candidate name
  3. No duplicate checking or candidate record linking occurs via API
  4. Each interview stores the candidate name independently

Example:

// Create interview with candidate name
const interview = await createInvitation({
  job_id: "job-123",
  candidate_name: "John Doe"
});
// Interview created with candidate_name: "John Doe"
If you need to track candidates centrally or avoid duplicates, manage candidate records through the Candidates API.

Candidate CV Data

You can optionally include candidate CV data for personalized interview experiences. When CV data is provided, the AI interviewer can:

  • Reference the candidate's work experience during follow-up questions
  • Tailor technical questions to the candidate's skill set
  • Ask about specific projects or certifications from their background

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:

FieldTypeDescription
summarystringProfessional summary or bio
workExperiencesarrayEmployment history
workExperiences[].companystringCompany name
workExperiences[].titlestringJob title
workExperiences[].startDatestringStart date (YYYY-MM format)
workExperiences[].endDatestringEnd date (YYYY-MM format)
workExperiences[].isCurrentbooleanWhether currently employed
workExperiences[].descriptionstringRole description
educationarrayEducational background
education[].institutionstringSchool/university name
education[].degreestringDegree type
education[].fieldstringField of study
skillsarrayTechnical and soft skills
skills[].namestringSkill name
skills[].categorystring"technical", "soft", or "language"
skills[].proficiencystring"beginner", "intermediate", "advanced", or "expert"
certificationsarrayProfessional certifications
linkedinUrlstringLinkedIn profile URL
portfolioUrlstringPortfolio or personal website
For best results, include at least the candidate's recent work experience and relevant technical skills. This data helps the AI generate more relevant follow-up questions.

For detailed CV data schemas and standalone candidate management, see the Candidates API.

Question Customization

Excluding Questions

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:

  1. Call GET /api/v1/jobs/:id to see all questions
  2. Note the id field for each question
  3. Pass those IDs in excluded_question_ids

Adding Custom Questions

Add 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 situations
  • technical: Domain knowledge and skills
  • situational: Problem-solving scenarios
  • experience: Past work experience
  • role_specific: Unique to the position

Time limits:

  • Recommended: 120-300 seconds (2-5 minutes)
  • Default: 180 seconds (3 minutes)
  • Affects credit calculation

Question Ordering

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:

  • Use job question UUIDs for pre-existing questions
  • Use custom_0, custom_1, etc. for custom questions (based on their index in the custom_questions array)
  • Questions not in the order array will be appended at the end
  • Excluded questions are ignored during the interview

Combined Customization

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"]
  }
}

Expiration Configuration

Set how many days until the interview link expires:

{
  "expires_in_days": 7
}

Format: Integer (1-90 days)

Recommendations:

  • 3-7 days: For engaged candidates
  • 14 days: For candidates who need more time
  • Custom: Based on your hiring timeline (max 90 days)

What happens on expiration:

  • If candidate hasn't started: Link becomes invalid, credits released
  • If candidate started: They can still complete

Available Voices

Round Zero offers a selection of professional ElevenLabs voices for interviews. You can specify which voice to use via the voice_id parameter.

Default Voice

If no voice_id is specified, interviews will use Rachel (Professional Female) by default.

Voice Options

Voice IDNameDescription
21m00Tcm4TlvDq8ikWAMRachelProfessional Female (Default)
AZnzlk1XvdvUeBnXmlldDomiConfident Female
EXAVITQu4vr4xnSDxMaLBellaWarm Female
ErXwobaYiN019PkySvjVAntoniWell-Rounded Male
MF3mGyEYCl7XYWbV9V6OElliEmotional Female
TxGEqnHWrfWFTfGW9XjXJoshDeep Male
VR6AewLTigWG4xSOukaGArnoldCrisp Male
pNInz6obpgDQGcFmaJgBAdamAmerican Male
yoZ06aMxZJJ28mfd3POQSamRaspy Male

Using a Specific Voice

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"
  }'
Custom Voices: If you have an active Employer Branding subscription, you can also use custom cloned voices. Custom voice IDs are returned when you create a voice clone via the dashboard.

Workflow Examples

ATS Integration

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
        }
    });
}

Bulk Interview Creation

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;
}

Best Practices

Idempotency

Always use idempotency keys for reliable integrations:

const idempotencyKey = `${sourceSystem}-${candidateId}-${jobId}`;

const interview = await createInterview({
    headers: { 'Idempotency-Key': idempotencyKey },
    // ... rest of request
});

Error Handling

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;
    }
}

Credit Monitoring

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

Next Steps