> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tiro.ooo/llms.txt
> Use this file to discover all available pages before exploring further.

# Tutorial

> Step-by-step guide to process audio files with transcription and translation

## Overview

This tutorial walks you through the complete process of uploading an audio file, getting it transcribed, and translated into multiple languages using the Tiro API.

## Prerequisites

* Valid Tiro API key
* Audio file (MP3, WAV, M4A)
* Max file size: 500MB
* Max duration: 4 hours

## Step 1: Create a Voice File Job

Start by creating a job with your desired transcription and translation settings:

<CodeGroup>
  ```bash cURL theme={"system"}
  curl -X POST https://api.tiro.ooo/v1/external/voice-file/jobs \
    -H "Authorization: Bearer $TIRO_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "transcriptLocaleHints": ["ko_KR"],
      "translationLocales": ["en_US"]
    }'
  ```

  ```javascript Node.js theme={"system"}
  async function createVoiceFileJob() {
    const response = await fetch('https://api.tiro.ooo/v1/external/voice-file/jobs', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.TIRO_API_KEY}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        transcriptLocaleHints: ['ko_KR'],
        translationLocales: ['en_US']
      })
    });
    
    if (!response.ok) {
      throw new Error(`Failed to create job: ${response.status}`);
    }
    
    const job = await response.json();
    console.log('Job created:', job.id);
    console.log('Upload URL:', job.uploadUri);
    
    return job;
  }
  ```

  ```python Python theme={"system"}
  import requests
  import os

  def create_voice_file_job():
      response = requests.post(
          'https://api.tiro.ooo/v1/external/voice-file/jobs',
          headers={
              'Authorization': f'Bearer {os.environ["TIRO_API_KEY"]}',
              'Content-Type': 'application/json'
          },
          json={
              'transcriptLocaleHints': ['ko_KR'],
              'translationLocales': ['en_US']
          }
      )
      
      response.raise_for_status()
      job = response.json()
      
      print(f'Job created: {job["id"]}')
      print(f'Upload URL: {job["uploadUri"]}')
      
      return job
  ```

  ```go Go theme={"system"}
  package main

  import (
      "bytes"
      "encoding/json"
      "fmt"
      "net/http"
      "os"
  )

  type CreateJobRequest struct {
      TranscriptLocaleHints []string `json:"transcriptLocaleHints"`
      TranslationLocales    []string `json:"translationLocales"`
  }

  type CreateJobResponse struct {
      ID        string `json:"id"`
      UploadUri string `json:"uploadUri"`
  }

  func createVoiceFileJob() (*CreateJobResponse, error) {
      reqBody := CreateJobRequest{
          TranscriptLocaleHints: []string{"ko_KR"},
          TranslationLocales:    []string{"en_US"},
      }
      
      jsonBody, _ := json.Marshal(reqBody)
      
      req, _ := http.NewRequest("POST", "https://api.tiro.ooo/v1/external/voice-file/jobs", bytes.NewBuffer(jsonBody))
      req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", os.Getenv("TIRO_API_KEY")))
      req.Header.Set("Content-Type", "application/json")
      
      client := &http.Client{}
      resp, err := client.Do(req)
      if err != nil {
          return nil, err
      }
      defer resp.Body.Close()
      
      var job CreateJobResponse
      json.NewDecoder(resp.Body).Decode(&job)
      
      fmt.Printf("Job created: %s\n", job.ID)
      fmt.Printf("Upload URL: %s\n", job.UploadUri)
      
      return &job, nil
  }
  ```

  ```kotlin Kotlin + Spring theme={"system"}
  import org.springframework.beans.factory.annotation.Value
  import org.springframework.http.*
  import org.springframework.web.client.RestTemplate

  data class CreateJobRequest(
      val transcriptLocaleHints: List<String>,
      val translationLocales: List<String>
  )

  data class CreateJobResponse(
      val id: String,
      val uploadUri: String
  )

  @Service
  class VoiceFileService {
      
      @Value("\${tiro.api.key}")
      private lateinit var apiKey: String
      
      private val restTemplate = RestTemplate()
      
      fun createVoiceFileJob(): CreateJobResponse {
          val headers = HttpHeaders().apply {
              set("Authorization", "Bearer $apiKey")
              contentType = MediaType.APPLICATION_JSON
          }
          
          val request = CreateJobRequest(
              transcriptLocaleHints = listOf("ko_KR"),
              translationLocales = listOf("en_US")
          )
          
          val entity = HttpEntity(request, headers)
          
          val response = restTemplate.exchange(
              "https://api.tiro.ooo/v1/external/voice-file/jobs",
              HttpMethod.POST,
              entity,
              CreateJobResponse::class.java
          )
          
          val job = response.body!!
          println("Job created: ${job.id}")
          println("Upload URL: ${job.uploadUri}")
          
          return job
      }
  }
  ```
</CodeGroup>

**Example Response:**

```json theme={"system"}
{
  "id": "b2d1ab32-3fe2-4201-b0b4-391abdbaa023",
  "uploadUri": "https://storage.example.com/upload/signed-url"
}
```

## Step 2: Upload Your Audio File

Upload your audio file to the provided signed URL:

<CodeGroup>
  ```bash cURL theme={"system"}
  # Upload audio file to the signed URL (replace UPLOAD_URI with the uploadUri from Step 1)
  curl -X PUT "$UPLOAD_URI" \
    -H "Content-Type: audio/mpeg" \
    --data-binary @/path/to/your/audio.mp3
  ```

  ```javascript Node.js theme={"system"}
  async function uploadAudioFile(uploadUri, audioFile) {
    const response = await fetch(uploadUri, {
      method: 'PUT',
      body: audioFile,
      headers: {
        'Content-Type': audioFile.type || 'audio/mpeg'
      }
    });
    
    if (!response.ok) {
      throw new Error(`Upload failed: ${response.status}`);
    }
    
    console.log('File uploaded successfully');
  }

  // For Node.js with file system
  const fs = require('fs');

  async function uploadAudioFileNode(uploadUri, filePath) {
    const fileBuffer = fs.readFileSync(filePath);
    
    const response = await fetch(uploadUri, {
      method: 'PUT',
      body: fileBuffer,
      headers: {
        'Content-Type': 'audio/mpeg'
      }
    });
    
    if (!response.ok) {
      throw new Error(`Upload failed: ${response.status}`);
    }
    
    console.log('File uploaded successfully');
  }
  ```

  ```python Python theme={"system"}
  def upload_audio_file(upload_uri, file_path):
      with open(file_path, 'rb') as audio_file:
          response = requests.put(
              upload_uri,
              data=audio_file,
              headers={'Content-Type': 'audio/mpeg'}
          )
      
      response.raise_for_status()
      print('File uploaded successfully')
  ```

  ```go Go theme={"system"}
  func uploadAudioFile(uploadUri, filePath string) error {
      file, err := os.Open(filePath)
      if err != nil {
          return err
      }
      defer file.Close()
      
      req, _ := http.NewRequest("PUT", uploadUri, file)
      req.Header.Set("Content-Type", "audio/mpeg")
      
      client := &http.Client{}
      resp, err := client.Do(req)
      if err != nil {
          return err
      }
      defer resp.Body.Close()
      
      if resp.StatusCode != http.StatusOK {
          return fmt.Errorf("upload failed: %d", resp.StatusCode)
      }
      
      fmt.Println("File uploaded successfully")
      return nil
  }
  ```

  ```kotlin Kotlin + Spring theme={"system"}
  fun uploadAudioFile(uploadUri: String, filePath: String) {
      val file = File(filePath)
      val headers = HttpHeaders().apply {
          contentType = MediaType.parseMediaType("audio/mpeg")
      }
      
      val resource = FileSystemResource(file)
      val entity = HttpEntity(resource, headers)
      
      val response = restTemplate.exchange(
          uploadUri,
          HttpMethod.PUT,
          entity,
          String::class.java
      )
      
      if (response.statusCode.is2xxSuccessful) {
          println("File uploaded successfully")
      } else {
          throw RuntimeException("Upload failed: ${response.statusCode}")
      }
  }
  ```
</CodeGroup>

## Step 3: Notify Upload Completion

Tell the API that the upload is complete:

<CodeGroup>
  ```bash cURL theme={"system"}
  curl -X PUT "https://api.tiro.ooo/v1/external/voice-file/jobs/$JOB_ID/upload-complete" \
    -H "Authorization: Bearer $TIRO_API_KEY" \
    -H "Content-Type: application/json"
  ```

  ```javascript Node.js theme={"system"}
  async function notifyUploadComplete(jobId) {
    const response = await fetch(
      `https://api.tiro.ooo/v1/external/voice-file/jobs/${jobId}/upload-complete`,
      {
        method: 'PUT',
        headers: {
          'Authorization': `Bearer ${process.env.TIRO_API_KEY}`,
          'Content-Type': 'application/json'
        }
      }
    );
    
    if (!response.ok) {
      throw new Error(`Failed to notify upload: ${response.status}`);
    }
    
    console.log('Upload notification sent');
  }
  ```

  ```python Python theme={"system"}
  def notify_upload_complete(job_id):
      response = requests.put(
          f'https://api.tiro.ooo/v1/external/voice-file/jobs/{job_id}/upload-complete',
          headers={
              'Authorization': f'Bearer {os.getenv("TIRO_API_KEY")}',
              'Content-Type': 'application/json'
          }
      )
      
      response.raise_for_status()
      print('Upload notification sent')
  ```

  ```go Go theme={"system"}
  func notifyUploadComplete(jobId string) error {
      url := fmt.Sprintf("https://api.tiro.ooo/v1/external/voice-file/jobs/%s/upload-complete", jobId)
      
      req, _ := http.NewRequest("PUT", url, nil)
      req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", os.Getenv("TIRO_API_KEY")))
      req.Header.Set("Content-Type", "application/json")
      
      client := &http.Client{}
      resp, err := client.Do(req)
      if err != nil {
          return err
      }
      defer resp.Body.Close()
      
      fmt.Println("Upload notification sent")
      return nil
  }
  ```

  ```kotlin Kotlin + Spring theme={"system"}
  fun notifyUploadComplete(jobId: String) {
      val headers = HttpHeaders().apply {
          set("Authorization", "Bearer $apiKey")
          contentType = MediaType.APPLICATION_JSON
      }
      
      val entity = HttpEntity<String>(headers)
      
      restTemplate.exchange(
          "https://api.tiro.ooo/v1/external/voice-file/jobs/$jobId/upload-complete",
          HttpMethod.PUT,
          entity,
          String::class.java
      )
      
      println("Upload notification sent")
  }
  ```
</CodeGroup>

## Step 4: Poll for Job Completion

Monitor the job status until processing is complete:

<CodeGroup>
  ```bash cURL theme={"system"}
  # Check job status (poll until status is COMPLETED or FAILED)
  curl -X GET "https://api.tiro.ooo/v1/external/voice-file/jobs/$JOB_ID" \
    -H "Authorization: Bearer $TIRO_API_KEY"

  # Example response:
  # {
  #   "id": "job_abc123",
  #   "status": "COMPLETED",
  #   "fileUploadedAt": "2024-01-01T12:00:20Z",
  #   "processStartedAt": "2024-01-01T12:01:00Z",
  #   "processCompletedAt": "2024-01-01T12:02:30Z"
  # }
  ```

  ```javascript Node.js theme={"system"}
  async function waitForJobCompletion(jobId) {
    let delay = 2000; // Start with 2 seconds
    const maxDelay = 30000; // Max 30 seconds
    const maxAttempts = 60; // 30 minutes max
    let attempts = 0;
    
    while (attempts < maxAttempts) {
      try {
        const response = await fetch(
          `https://api.tiro.ooo/v1/external/voice-file/jobs/${jobId}`,
          {
            headers: {
              'Authorization': `Bearer ${process.env.TIRO_API_KEY}`
            }
          }
        );
        
        if (!response.ok) {
          throw new Error(`Failed to get job status: ${response.status}`);
        }
        
        const job = await response.json();
        console.log(`Job status: ${job.status}`);
        
        if (job.status === 'COMPLETED') {
          console.log('Job completed successfully!');
          return job;
        }
        
        if (job.status === 'FAILED') {
          throw new Error(`Job failed: ${job.errorMessage || 'Unknown error'}`);
        }
        
        // Still processing - wait with exponential backoff
        await new Promise(resolve => setTimeout(resolve, delay));
        delay = Math.min(delay * 1.2, maxDelay);
        attempts++;
        
      } catch (error) {
        console.error('Error checking job status:', error);
        attempts++;
        await new Promise(resolve => setTimeout(resolve, delay));
        delay = Math.min(delay * 1.5, maxDelay);
      }
    }
    
    throw new Error('Job did not complete within timeout');
  }
  ```

  ```python Python theme={"system"}
  import time
  import asyncio

  async def wait_for_job_completion(job_id):
      delay = 2  # Start with 2 seconds
      max_delay = 30  # Max 30 seconds
      max_attempts = 60  # 30 minutes max
      attempts = 0
      
      while attempts < max_attempts:
          try:
              response = requests.get(
                  f'https://api.tiro.ooo/v1/external/voice-file/jobs/{job_id}',
                  headers={
                      'Authorization': f'Bearer {os.getenv("TIRO_API_KEY")}'
                  }
              )
              
              response.raise_for_status()
              job = response.json()
              print(f'Job status: {job["status"]}')
              
              if job['status'] == 'COMPLETED':
                  print('Job completed successfully!')
                  return job
                  
              if job['status'] == 'FAILED':
                  error_msg = job.get('errorMessage', 'Unknown error')
                  raise Exception(f'Job failed: {error_msg}')
                  
              # Still processing - wait with exponential backoff
              await asyncio.sleep(delay)
              delay = min(delay * 1.2, max_delay)
              attempts += 1
              
          except Exception as e:
              print(f'Error checking job status: {e}')
              attempts += 1
              await asyncio.sleep(delay)
              delay = min(delay * 1.5, max_delay)
      
      raise Exception('Job did not complete within timeout')
  ```

  ```go Go theme={"system"}
  type JobStatus struct {
      ID        string `json:"id"`
      Status    string `json:"status"`
      ErrorMessage string `json:"errorMessage,omitempty"`
  }

  func waitForJobCompletion(jobId string) (*JobStatus, error) {
      delay := 2 * time.Second
      maxDelay := 30 * time.Second
      maxAttempts := 60
      
      client := &http.Client{}
      url := fmt.Sprintf("https://api.tiro.ooo/v1/external/voice-file/jobs/%s", jobId)
      
      for attempts := 0; attempts < maxAttempts; attempts++ {
          req, _ := http.NewRequest("GET", url, nil)
          req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", os.Getenv("TIRO_API_KEY")))
          
          resp, err := client.Do(req)
          if err != nil {
              time.Sleep(delay)
              delay = time.Duration(float64(delay) * 1.5)
              if delay > maxDelay {
                  delay = maxDelay
              }
              continue
          }
          
          var job JobStatus
          json.NewDecoder(resp.Body).Decode(&job)
          resp.Body.Close()
          
          fmt.Printf("Job status: %s\n", job.Status)
          
          if job.Status == "COMPLETED" {
              fmt.Println("Job completed successfully!")
              return &job, nil
          }
          
          if job.Status == "FAILED" {
              return nil, fmt.Errorf("job failed: %s", job.ErrorMessage)
          }
          
          time.Sleep(delay)
          delay = time.Duration(float64(delay) * 1.2)
          if delay > maxDelay {
              delay = maxDelay
          }
      }
      
      return nil, fmt.Errorf("job did not complete within timeout")
  }
  ```

  ```kotlin Kotlin + Spring theme={"system"}
  import java.util.concurrent.TimeUnit

  data class JobStatus(
      val id: String,
      val status: String,
      val errorMessage: String? = null
  )

  fun waitForJobCompletion(jobId: String): JobStatus {
      var delay = 2000L // 2 seconds
      val maxDelay = 30000L // 30 seconds
      val maxAttempts = 60
      
      repeat(maxAttempts) { attempt ->
          try {
              val headers = HttpHeaders().apply {
                  set("Authorization", "Bearer $apiKey")
              }
              
              val entity = HttpEntity<String>(headers)
              
              val response = restTemplate.exchange(
                  "https://api.tiro.ooo/v1/external/voice-file/jobs/$jobId",
                  HttpMethod.GET,
                  entity,
                  JobStatus::class.java
              )
              
              val job = response.body!!
              println("Job status: ${job.status}")
              
              when (job.status) {
                  "COMPLETED" -> {
                      println("Job completed successfully!")
                      return job
                  }
                  "FAILED" -> {
                      throw RuntimeException("Job failed: ${job.errorMessage}")
                  }
              }
              
              TimeUnit.MILLISECONDS.sleep(delay)
              delay = minOf((delay * 1.2).toLong(), maxDelay)
              
          } catch (e: Exception) {
              println("Error checking job status: ${e.message}")
              TimeUnit.MILLISECONDS.sleep(delay)
              delay = minOf((delay * 1.5).toLong(), maxDelay)
          }
      }
      
      throw RuntimeException("Job did not complete within timeout")
  }
  ```
</CodeGroup>

## Step 5: Retrieve Results

Once the job is completed, fetch the transcript and translations:

<CodeGroup>
  ```bash cURL theme={"system"}
  # Get transcript
  curl -X GET "https://api.tiro.ooo/v1/external/voice-file/jobs/$JOB_ID/transcript" \
    -H "Authorization: Bearer $TIRO_API_KEY"

  # Get all translations
  curl -X GET "https://api.tiro.ooo/v1/external/voice-file/jobs/$JOB_ID/translations" \
    -H "Authorization: Bearer $TIRO_API_KEY"

  # Get specific translation (e.g., English)
  curl -X GET "https://api.tiro.ooo/v1/external/voice-file/jobs/$JOB_ID/translations/en_US" \
    -H "Authorization: Bearer $TIRO_API_KEY"

  # Get transcript paragraph summary
  curl -X GET "https://api.tiro.ooo/v1/external/voice-file/jobs/$JOB_ID/transcript/paragraph-summary" \
    -H "Authorization: Bearer $TIRO_API_KEY"

  # Get translation paragraph summary
  curl -X GET "https://api.tiro.ooo/v1/external/voice-file/jobs/$JOB_ID/translations/en_US/paragraph-summary" \
    -H "Authorization: Bearer $TIRO_API_KEY"
  ```

  ```javascript Node.js theme={"system"}
  async function getJobResults(jobId) {
    // Get transcript
    const transcriptResponse = await fetch(
      `https://api.tiro.ooo/v1/external/voice-file/jobs/${jobId}/transcript`,
      {
        headers: {
          'Authorization': `Bearer ${process.env.TIRO_API_KEY}`
        }
      }
    );
    
    if (!transcriptResponse.ok) {
      throw new Error(`Failed to get transcript: ${transcriptResponse.status}`);
    }
    
    const transcript = await transcriptResponse.json();
    console.log('Transcript:', transcript.text);
    
    // Get available translations
    const translationsResponse = await fetch(
      `https://api.tiro.ooo/v1/external/voice-file/jobs/${jobId}/translations`,
      {
        headers: {
          'Authorization': `Bearer ${process.env.TIRO_API_KEY}`
        }
      }
    );
    
    if (!translationsResponse.ok) {
      throw new Error(`Failed to get translations: ${translationsResponse.status}`);
    }
    
    const translations = await translationsResponse.json();
    
    // Get each translation
    const translationResults = {};
    for (const translation of translations) {
      translationResults[translation.locale] = translation.text;
    }
    
    // Get paragraph summaries
    const transcriptSummaryResponse = await fetch(
      `https://api.tiro.ooo/v1/external/voice-file/jobs/${jobId}/transcript/paragraph-summary`,
      {
        headers: {
          'Authorization': `Bearer ${process.env.TIRO_API_KEY}`
        }
      }
    );
    
    let transcriptSummary = null;
    if (transcriptSummaryResponse.ok) {
      transcriptSummary = await transcriptSummaryResponse.json();
    }

    // Get translation summaries
    const translationSummaries = {};
    for (const translation of translations) {
      const summaryResponse = await fetch(
        `https://api.tiro.ooo/v1/external/voice-file/jobs/${jobId}/translations/${translation.locale}/paragraph-summary`,
        {
          headers: {
            'Authorization': `Bearer ${process.env.TIRO_API_KEY}`
          }
        }
      );
      
      if (summaryResponse.ok) {
        const summaryData = await summaryResponse.json();
        translationSummaries[translation.locale] = summaryData.summary;
      }
    }

    return {
      transcript: transcript.text,
      translations: translationResults,
      transcriptSummary: transcriptSummary?.summary,
      translationSummaries
    };
  }
  ```

  ```python Python theme={"system"}
  def get_job_results(job_id):
      # Get transcript
      transcript_response = requests.get(
          f'https://api.tiro.ooo/v1/external/voice-file/jobs/{job_id}/transcript',
          headers={'Authorization': f'Bearer {os.getenv("TIRO_API_KEY")}'}
      )
      transcript_response.raise_for_status()
      transcript = transcript_response.json()
      
      print(f'Transcript: {transcript["text"]}')
      
      # Get available translations
      translations_response = requests.get(
          f'https://api.tiro.ooo/v1/external/voice-file/jobs/{job_id}/translations',
          headers={'Authorization': f'Bearer {os.getenv("TIRO_API_KEY")}'}
      )
      translations_response.raise_for_status()
      translations = translations_response.json()
      
      # Get each translation
      translation_results = {}
      for translation in translations:
          translation_results[translation['locale']] = translation['text']
      
      # Get paragraph summaries
      transcript_summary = None
      try:
          transcript_summary_response = requests.get(
              f'https://api.tiro.ooo/v1/external/voice-file/jobs/{job_id}/transcript/paragraph-summary',
              headers={'Authorization': f'Bearer {os.getenv("TIRO_API_KEY")}'}
          )
          if transcript_summary_response.ok:
              transcript_summary_data = transcript_summary_response.json()
              transcript_summary = transcript_summary_data['summary']
      except:
          pass

      # Get translation summaries
      translation_summaries = {}
      for translation in translations:
          try:
              summary_response = requests.get(
                  f'https://api.tiro.ooo/v1/external/voice-file/jobs/{job_id}/translations/{translation["locale"]}/paragraph-summary',
                  headers={'Authorization': f'Bearer {os.getenv("TIRO_API_KEY")}'}
              )
              
              if summary_response.ok:
                  summary_data = summary_response.json()
                  translation_summaries[translation['locale']] = summary_data['summary']
          except:
              pass
      
      return {
          'transcript': transcript['text'],
          'translations': translation_results,
          'transcript_summary': transcript_summary,
          'translation_summaries': translation_summaries
      }
  ```

  ```go Go theme={"system"}
  type Transcript struct {
      JobId   string   `json:"jobId"`
      Locales []string `json:"locales"`
      Text    string   `json:"text"`
  }

  type Translation struct {
      JobId  string `json:"jobId"`
      Locale string `json:"locale"`
      Text   string `json:"text"`
  }

  type JobResults struct {
      Transcript   string
      Translations map[string]string
  }

  func getJobResults(jobId string) (*JobResults, error) {
      client := &http.Client{}
      apiKey := os.Getenv("TIRO_API_KEY")
      
      // Get transcript
      transcriptUrl := fmt.Sprintf("https://api.tiro.ooo/v1/external/voice-file/jobs/%s/transcript", jobId)
      req, _ := http.NewRequest("GET", transcriptUrl, nil)
      req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
      
      resp, err := client.Do(req)
      if err != nil {
          return nil, err
      }
      defer resp.Body.Close()
      
      var transcript Transcript
      json.NewDecoder(resp.Body).Decode(&transcript)
      fmt.Printf("Transcript: %s\n", transcript.Text)
      
      // Get translations
      translationsUrl := fmt.Sprintf("https://api.tiro.ooo/v1/external/voice-file/jobs/%s/translations", jobId)
      req, _ = http.NewRequest("GET", translationsUrl, nil)
      req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
      
      resp, err = client.Do(req)
      if err != nil {
          return nil, err
      }
      defer resp.Body.Close()
      
      var translations []Translation
      json.NewDecoder(resp.Body).Decode(&translations)
      
      translationResults := make(map[string]string)
      for _, t := range translations {
          translationResults[t.Locale] = t.Text
      }
      
      return &JobResults{
          Transcript:   transcript.Text,
          Translations: translationResults,
      }, nil
  }
  ```

  ```kotlin Kotlin + Spring theme={"system"}
  data class Transcript(
      val jobId: String,
      val locales: List<String>,
      val text: String
  )

  data class Translation(
      val jobId: String,
      val locale: String,
      val text: String
  )

  data class JobResults(
      val transcript: String,
      val translations: Map<String, String>
  )

  fun getJobResults(jobId: String): JobResults {
      val headers = HttpHeaders().apply {
          set("Authorization", "Bearer $apiKey")
      }
      val entity = HttpEntity<String>(headers)
      
      // Get transcript
      val transcriptResponse = restTemplate.exchange(
          "https://api.tiro.ooo/v1/external/voice-file/jobs/$jobId/transcript",
          HttpMethod.GET,
          entity,
          Transcript::class.java
      )
      val transcript = transcriptResponse.body!!
      println("Transcript: ${transcript.text}")
      
      // Get translations
      val translationsResponse = restTemplate.exchange(
          "https://api.tiro.ooo/v1/external/voice-file/jobs/$jobId/translations",
          HttpMethod.GET,
          entity,
          object : ParameterizedTypeReference<List<Translation>>() {}
      )
      val translations = translationsResponse.body!!
      
      val translationResults = translations.associate { it.locale to it.text }
      
      return JobResults(
          transcript = transcript.text,
          translations = translationResults
      )
  }
  ```
</CodeGroup>

## Complete Example

Here's a complete example that processes an audio file from start to finish:

<CodeGroup>
  ```bash cURL theme={"system"}
  #!/bin/bash
  # Complete Voice File Processing Script

  # Set your API key
  export TIRO_API_KEY="your_api_key_here"
  AUDIO_FILE="/path/to/your/audio.mp3"

  # Step 1: Create job
  echo "Creating voice file job..."
  RESPONSE=$(curl -s -X POST https://api.tiro.ooo/v1/external/voice-file/jobs \
    -H "Authorization: Bearer $TIRO_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "transcriptLocaleHints": ["ko_KR"],
      "translationLocales": ["en_US"]
    }')

  JOB_ID=$(echo $RESPONSE | jq -r '.id')
  UPLOAD_URI=$(echo $RESPONSE | jq -r '.uploadUri')
  echo "Job created: $JOB_ID"

  # Step 2: Upload file
  echo "Uploading audio file..."
  curl -s -X PUT "$UPLOAD_URI" \
    -H "Content-Type: audio/mpeg" \
    --data-binary @"$AUDIO_FILE"
  echo "File uploaded successfully"

  # Step 3: Notify upload complete
  echo "Notifying upload completion..."
  curl -s -X PUT "https://api.tiro.ooo/v1/external/voice-file/jobs/$JOB_ID/upload-complete" \
    -H "Authorization: Bearer $TIRO_API_KEY" \
    -H "Content-Type: application/json"

  # Step 4: Poll for completion
  echo "Waiting for processing to complete..."
  while true; do
    STATUS=$(curl -s -X GET "https://api.tiro.ooo/v1/external/voice-file/jobs/$JOB_ID" \
      -H "Authorization: Bearer $TIRO_API_KEY" | jq -r '.status')
    echo "Status: $STATUS"
    
    if [ "$STATUS" = "COMPLETED" ]; then
      echo "Job completed successfully!"
      break
    elif [ "$STATUS" = "FAILED" ]; then
      echo "Job failed!"
      exit 1
    fi
    
    sleep 5
  done

  # Step 5: Get results
  echo "Retrieving results..."

  echo "=== Transcript ==="
  curl -s -X GET "https://api.tiro.ooo/v1/external/voice-file/jobs/$JOB_ID/transcript" \
    -H "Authorization: Bearer $TIRO_API_KEY" | jq '.text'

  echo "=== Translation (en_US) ==="
  curl -s -X GET "https://api.tiro.ooo/v1/external/voice-file/jobs/$JOB_ID/translations/en_US" \
    -H "Authorization: Bearer $TIRO_API_KEY" | jq '.text'

  echo "=== Paragraph Summary ==="
  curl -s -X GET "https://api.tiro.ooo/v1/external/voice-file/jobs/$JOB_ID/transcript/paragraph-summary" \
    -H "Authorization: Bearer $TIRO_API_KEY" | jq '.summary'

  echo "Processing completed!"
  ```

  ```javascript Node.js theme={"system"}
  async function processAudioFile(audioFile) {
    try {
      // Step 1: Create job
      console.log('Creating voice file job...');
      const job = await createVoiceFileJob();
      
      // Step 2: Upload file
      console.log('Uploading audio file...');
      await uploadAudioFile(job.uploadUri, audioFile);
      
      // Step 3: Notify upload complete
      console.log('Notifying upload completion...');
      await notifyUploadComplete(job.id);
      
      // Step 4: Wait for processing
      console.log('Waiting for processing to complete...');
      const completedJob = await waitForJobCompletion(job.id);
      
      // Step 5: Get results
      console.log('Retrieving results...');
      const results = await getJobResults(job.id);
      
      console.log('Processing completed!');
      console.log('Transcript:', results.transcript);
      console.log('Translations:', results.translations);
      console.log('Transcript Summary:', results.transcriptSummary);
      console.log('Translation Summaries:', results.translationSummaries);
      
      return results;
      
    } catch (error) {
      console.error('Error processing audio file:', error);
      throw error;
    }
  }

  // Usage
  processAudioFile(myAudioFile)
    .then(results => {
      // Handle successful processing
      displayResults(results);
    })
    .catch(error => {
      // Handle error
      showError(error.message);
    });
  ```

  ```python Python theme={"system"}
  async def process_audio_file(file_path):
      try:
          # Step 1: Create job
          print('Creating voice file job...')
          job = create_voice_file_job()
          
          # Step 2: Upload file
          print('Uploading audio file...')
          upload_audio_file(job['uploadUri'], file_path)
          
          # Step 3: Notify upload complete
          print('Notifying upload completion...')
          notify_upload_complete(job['id'])
          
          # Step 4: Wait for processing
          print('Waiting for processing to complete...')
          completed_job = await wait_for_job_completion(job['id'])
          
          # Step 5: Get results
          print('Retrieving results...')
          results = get_job_results(job['id'])
          
          print('Processing completed!')
          print(f'Transcript: {results["transcript"]}')
          print(f'Translations: {results["translations"]}')
          print(f'Transcript Summary: {results["transcript_summary"]}')
          print(f'Translation Summaries: {results["translation_summaries"]}')
          
          return results
          
      except Exception as e:
          print(f'Error processing audio file: {e}')
          raise

  # Usage
  if __name__ == '__main__':
      asyncio.run(process_audio_file('path/to/audio.mp3'))
  ```
</CodeGroup>

## Step 6: Understanding Paragraph Summaries

The Paragraph Summary feature provides intelligent content summarization that helps you quickly understand the key points from your audio files.

### What are Paragraph Summaries?

Paragraph summaries are automatically generated overviews of your transcript or translation content, broken down into logical sections. Each summary provides:

* **Concise Overview**: Key points from each paragraph in markdown format
* **Language-Specific**: Summaries generated for both transcript and translations
* **Structured Content**: Easy to parse and integrate into your applications

### Example Response

When you fetch paragraph summaries, you'll receive a response like this:

```json theme={"system"}
{
  "jobId": "job-123",
  "locale": "ko_KR",
  "summary": [
    {
      "type": "markdown",
      "content": "### 회의는 프로젝트 진행 상황을 점검하고, 다음 단계에 대한 계획을 수립하는 데 중점을 두었습니다."
    },
    {
      "type": "markdown", 
      "content": "### 팀원들은 각자의 담당 업무에 대한 현재 상태를 보고하고, 발생한 이슈들에 대해 논의했습니다."
    }
  ]
}
```

### When are Summaries Available?

* **Transcript and Translation Summaries**: Available after job completion (`COMPLETED` status)
  * `COMPLETED` means all processing is finished: transcript, translation (if requested), and paragraph summaries for both
* **Processing Time**: Summaries are generated asynchronously, usually within 30-60 seconds after the main processing

## Polling Best Practices

### Exponential Backoff Strategy

* **Start delay**: 2 seconds (processing takes time to start)
* **Growth factor**: 1.2x (gentle increase)
* **Maximum delay**: 30 seconds (avoid overwhelming the server)
* **Maximum attempts**: Based on expected processing time

### Job State Transitions

```mermaid theme={"system"}
stateDiagram-v2
    [*] --> CREATED: Create job
    CREATED --> UPLOADED: Upload file
    UPLOADED --> PROCESSING: Start processing
    PROCESSING --> COMPLETED: Success
    PROCESSING --> FAILED: Error occurred
    COMPLETED --> [*]
    FAILED --> [*]
```

### Processing Time Guidelines

| File Duration  | Expected Processing Time | Recommended Poll Interval |
| -------------- | ------------------------ | ------------------------- |
| \< 5 minutes   | 30-60 seconds            | Start: 2s, Max: 10s       |
| 5-30 minutes   | 1-5 minutes              | Start: 5s, Max: 20s       |
| 30-120 minutes | 5-15 minutes             | Start: 10s, Max: 30s      |
| > 2 hours      | 15-30 minutes            | Start: 30s, Max: 60s      |

## Common Issues & Solutions

### Upload Failures

* **Issue**: 413 Payload Too Large
* **Solution**: Check file size (max 500MB) and compress if needed

### Processing Failures

* **Issue**: Low audio quality
* **Solution**: Ensure sample rate ≥8kHz and minimal background noise

### Timeout Issues

* **Issue**: Job doesn't complete within expected time
* **Solution**: Increase timeout for longer files, check job state for errors

If you need to revisit the bigger picture, see the [Voice File Overview](/en/developers/voice-file/overview); for every parameter and response field, jump into the [API Reference](/api-reference/voice-file/create-voice-file-job).
