Your first request
A complete end-to-end integration, showing all the pieces you'll need in production:
- Submit the audio
- Poll with escalating intervals
- Handle queued / processing / terminal statuses
- Respect
Retry-Afteron 429s - Extract and use the scores
Speech module example
This walks through a full integration using the Speech module. The same
X-API-Key auth and async submit → poll pattern applies to every module —
see CV DeepMatch, CV DeepSearch,
and Interview for their endpoints and specifics.
Full working example
- Python
- Node.js
- cURL
- Go
import os
import time
import requests
API_BASE = "https://platform.zenhire.ai"
API_KEY = os.environ["ZENHIRE_API_KEY"] # zh_…
AUDIO = "interview.mp3"
def analyze(audio_path: str, external_id: str | None = None) -> dict:
# 1. Submit
with open(audio_path, "rb") as f:
data = {"language": "en"}
if external_id:
data["externalId"] = external_id
r = requests.post(
f"{API_BASE}/api/v1/speech/analyze",
headers={"X-API-Key": API_KEY},
files={"audio": f},
data=data,
timeout=60,
)
r.raise_for_status()
submit = r.json()
request_id = submit["id"]
print(f"Submitted: {request_id} (status={submit['status']})")
# 2. Poll
start = time.time()
interval = submit.get("pollIntervalSeconds", 15)
while time.time() - start < 20 * 60: # 20-minute budget
time.sleep(interval)
resp = requests.get(
f"{API_BASE}/api/v1/speech/analyze/{request_id}",
headers={"X-API-Key": API_KEY},
timeout=30,
)
if resp.status_code == 429:
retry_after = int(resp.headers.get("Retry-After", interval))
time.sleep(retry_after)
continue
resp.raise_for_status()
data = resp.json()
status = data["status"]
if status in ("success", "partial"):
return data
if status == "failed":
err = data.get("error") or {}
raise RuntimeError(f"{err.get('code')}: {err.get('message')}")
# Escalate: 15s for first 2 min, then 30s, then 60s
elapsed = time.time() - start
interval = 15 if elapsed < 120 else 30 if elapsed < 300 else 60
raise TimeoutError(
f"Polling budget exhausted for {request_id} "
"— the id is still valid, you can resume later."
)
if __name__ == "__main__":
result = analyze(AUDIO, external_id="candidate-abc-123")
s = result["scores"]
print(f"Overall: {s['overall']} ({s['cefrLevel']})")
print(f" Vocab: {s['vocabulary']} ({s['vocabularyCefr']})")
print(f" Fluency: {s['fluency']} ({s['fluencyCefr']})")
print(f" Accent: {s['accent']} ({s['accentCefr']})")
print(f"\n{result['hrRecommendation']}")
import fs from "node:fs";
const API_BASE = "https://platform.zenhire.ai";
const API_KEY = process.env.ZENHIRE_API_KEY;
const AUDIO = "interview.mp3";
async function analyze(audioPath, externalId) {
// 1. Submit
const form = new FormData();
form.append("audio", new Blob([fs.readFileSync(audioPath)]), audioPath);
form.append("language", "en");
if (externalId) form.append("externalId", externalId);
const submitRes = await fetch(`${API_BASE}/api/v1/speech/analyze`, {
method: "POST",
headers: { "X-API-Key": API_KEY },
body: form,
});
if (!submitRes.ok) {
throw new Error(`Submit failed: ${submitRes.status}`);
}
const submit = await submitRes.json();
console.log(`Submitted: ${submit.id} (status=${submit.status})`);
// 2. Poll
const start = Date.now();
let interval = (submit.pollIntervalSeconds ?? 15) * 1000;
while (Date.now() - start < 20 * 60_000) {
await new Promise((r) => setTimeout(r, interval));
const res = await fetch(
`${API_BASE}/api/v1/speech/analyze/${submit.id}`,
{ headers: { "X-API-Key": API_KEY } },
);
if (res.status === 429) {
const retryAfter = parseInt(res.headers.get("Retry-After") ?? "15", 10);
await new Promise((r) => setTimeout(r, retryAfter * 1000));
continue;
}
if (!res.ok) throw new Error(`Poll failed: ${res.status}`);
const data = await res.json();
if (data.status === "success" || data.status === "partial") return data;
if (data.status === "failed") {
throw new Error(`${data.error?.code}: ${data.error?.message}`);
}
// Escalate: 15s → 30s → 60s
const elapsed = Date.now() - start;
interval = elapsed < 120_000 ? 15_000 : elapsed < 300_000 ? 30_000 : 60_000;
}
throw new Error("Polling budget exhausted");
}
const result = await analyze(AUDIO, "candidate-abc-123");
const s = result.scores;
console.log(`Overall: ${s.overall} (${s.cefrLevel})`);
console.log(` Vocab: ${s.vocabulary} (${s.vocabularyCefr})`);
console.log(` Fluency: ${s.fluency} (${s.fluencyCefr})`);
console.log(` Accent: ${s.accent} (${s.accentCefr})`);
console.log(`\n${result.hrRecommendation}`);
#!/usr/bin/env bash
set -euo pipefail
API_BASE="https://platform.zenhire.ai"
API_KEY="${ZENHIRE_API_KEY:?set ZENHIRE_API_KEY}"
AUDIO="interview.mp3"
# 1. Submit
SUBMIT=$(curl -sS -X POST "$API_BASE/api/v1/speech/analyze" \
-H "X-API-Key: $API_KEY" \
-F "audio=@$AUDIO" \
-F "language=en" \
-F "externalId=candidate-abc-123")
REQUEST_ID=$(echo "$SUBMIT" | python3 -c "import sys, json; print(json.load(sys.stdin)['id'])")
echo "Submitted: $REQUEST_ID"
# 2. Poll
INTERVAL=15
START=$(date +%s)
while true; do
sleep "$INTERVAL"
NOW=$(date +%s)
if (( NOW - START > 20 * 60 )); then
echo "Polling budget exhausted"
exit 1
fi
RESP=$(curl -sS -w "\n%{http_code}" "$API_BASE/api/v1/speech/analyze/$REQUEST_ID" \
-H "X-API-Key: $API_KEY")
HTTP_CODE=$(echo "$RESP" | tail -1)
BODY=$(echo "$RESP" | sed '$d')
if [[ "$HTTP_CODE" == "429" ]]; then
sleep 15; continue
fi
STATUS=$(echo "$BODY" | python3 -c "import sys, json; print(json.load(sys.stdin)['status'])")
case "$STATUS" in
success|partial)
echo "$BODY" | python3 -m json.tool
exit 0 ;;
failed)
echo "Analysis failed: $BODY"
exit 1 ;;
esac
ELAPSED=$(( $(date +%s) - START ))
INTERVAL=$(( ELAPSED < 120 ? 15 : ELAPSED < 300 ? 30 : 60 ))
done
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"strconv"
"time"
)
func main() {
apiBase := "https://platform.zenhire.ai"
apiKey := os.Getenv("ZENHIRE_API_KEY")
// 1. Submit
body := &bytes.Buffer{}
w := multipart.NewWriter(body)
f, _ := os.Open("interview.mp3")
defer f.Close()
part, _ := w.CreateFormFile("audio", "interview.mp3")
io.Copy(part, f)
w.WriteField("language", "en")
w.WriteField("externalId", "candidate-abc-123")
w.Close()
req, _ := http.NewRequest("POST", apiBase+"/api/v1/speech/analyze", body)
req.Header.Set("X-API-Key", apiKey)
req.Header.Set("Content-Type", w.FormDataContentType())
resp, _ := http.DefaultClient.Do(req)
var submit map[string]any
json.NewDecoder(resp.Body).Decode(&submit)
resp.Body.Close()
requestID := submit["id"].(string)
fmt.Println("Submitted:", requestID)
// 2. Poll
start := time.Now()
interval := 15 * time.Second
for time.Since(start) < 20*time.Minute {
time.Sleep(interval)
pollReq, _ := http.NewRequest("GET", apiBase+"/api/v1/speech/analyze/"+requestID, nil)
pollReq.Header.Set("X-API-Key", apiKey)
pollResp, _ := http.DefaultClient.Do(pollReq)
if pollResp.StatusCode == 429 {
retryAfter, _ := strconv.Atoi(pollResp.Header.Get("Retry-After"))
if retryAfter == 0 {
retryAfter = 15
}
time.Sleep(time.Duration(retryAfter) * time.Second)
pollResp.Body.Close()
continue
}
var data map[string]any
json.NewDecoder(pollResp.Body).Decode(&data)
pollResp.Body.Close()
status := data["status"].(string)
if status == "success" || status == "partial" {
b, _ := json.MarshalIndent(data, "", " ")
fmt.Println(string(b))
return
}
if status == "failed" {
fmt.Println("Failed:", data["error"])
os.Exit(1)
}
elapsed := time.Since(start)
if elapsed < 2*time.Minute {
interval = 15 * time.Second
} else if elapsed < 5*time.Minute {
interval = 30 * time.Second
} else {
interval = 60 * time.Second
}
}
fmt.Println("Polling budget exhausted")
}
What this example gets right
- Escalating poll intervals — starts at 15s, backs off as the run takes longer. Don't hammer every 5 seconds.
- Handles 429 on poll — respects the
Retry-Afterheader. - Raises on
failed, returns onsuccess/partial— bothsuccessandpartialcontain usable scores. - Sets
externalId— your own correlation tag, echoed back on every response. Filterable onGET /api/v1/speech/runs. Not unique, so safe to reuse across retries. - Does not treat
queuedorprocessingas errors — they're normal mid-run states. - 20-minute overall budget, then exits cleanly — the run
iddoes not expire, so you can resume later.
Next
- Async polling flow — deeper discussion of intervals, back-off, and webhooks.
- Error handling — what to do on 402, 403, 503.
- POST /api/v1/speech/analyze — submit endpoint reference.
- GET /api/v1/speech/analyze/{id} — poll endpoint reference.
- More recipes: cURL, Python, Node.js.
- Other modules: CV DeepMatch · CV DeepSearch · Interview — same async pattern, different endpoints.