Voice authenticity analysis over HTTP. Send an audio file with your API key and a consent attestation, and receive a result. /api/v1/verdict-only returns the code authentic, synthetic or inconclusive. authentic means no strong synthetic signal was found; it is not a certification of authenticity. Results are advisory.
Every call carries your API key in the X-API-Key header, or as Authorization: Bearer <key>. A key sent in the query string or in a form field is refused. Create and revoke keys on the API keys page.
# Live key oravys_live_... # Test key: on POST /api/v1/analyze it returns a # sample response without running an analysis oravys_test_...
X-API-Key: oravys_live_... # or Authorization: Bearer oravys_live_...
A voice is biometric data. Every call that sends audio must carry consent=true, or the pair consent_data_processing=true and consent_voice_processing=true. By sending it you attest that the person whose voice is analyzed gave explicit consent (GDPR Art. 9.2.a, BIPA). Without it the API answers 400 consent_required, and the call is not counted against your quota.
The analysis routes require the detect:audio scope on the key. A key without it receives 403 insufficient_permissions. Keys issued before scopes existed keep their access.
All requests go to https://app.oravys.com. Audio is sent as multipart/form-data in the field audio (wav, mp3, flac, m4a, ogg, webm). The full contract is in the OpenAPI specification.
| Route | Use | Answer |
|---|---|---|
| POST /api/v1/verdict-only | Fast verdict, no report. The call waits for the detector, up to about 90 seconds. Audio up to 25 MB. 10 calls per hour per key. | verdict, confidence (0 to 100), verdict_label |
| POST /api/v1/analyze/async | Full analysis as a background job. Audio up to 15 MB. | 202 with job_id and poll_url |
| GET /api/v1/job/{job_id} | Status of a job: pending, running, completed, failed. Only the key that submitted it can read it. | report_html once completed |
| DELETE /api/v1/job/{job_id} | Cancel a job. | status: cancelled |
| POST /api/v1/analyze | Full analysis in one call. The response is streamed: a space every 15 seconds, then the JSON result on the last line. The server stops after about 270 seconds; prefer the async route for long recordings. | verdict, confidence_score (0 to 1), report_url |
| GET /api/v1/health | Service status. No key required. | status |
Retries: send an Idempotency-Key header (8 to 128 characters) on POST requests. The same key with the same body within 24 hours returns the first answer; the same key with a different body answers 422 idempotency_conflict. The streamed answer of /api/v1/analyze is not replayed.
Submit a recording as a job, then poll it. Replace the key and the file name.
# Fast verdict curl -X POST https://app.oravys.com/api/v1/verdict-only \ -H "X-API-Key: $ORAVYS_API_KEY" \ -F "audio=@recording.wav" \ -F "consent=true" # Full analysis as a job curl -X POST https://app.oravys.com/api/v1/analyze/async \ -H "X-API-Key: $ORAVYS_API_KEY" \ -H "Idempotency-Key: 5f0c2d6e-recording-01" \ -F "audio=@recording.wav" \ -F "consent=true" # Poll until status is completed or failed curl https://app.oravys.com/api/v1/job/JOB_ID \ -H "X-API-Key: $ORAVYS_API_KEY"
import os, time, requests BASE = "https://app.oravys.com" HEADERS = {"X-API-Key": os.environ["ORAVYS_API_KEY"]} with open("recording.wav", "rb") as f: r = requests.post( f"{BASE}/api/v1/analyze/async", headers=HEADERS, files={"audio": f}, data={"consent": "true"}, ) if r.status_code >= 400: err = r.json()["error"] raise SystemExit(f"{err['code']}: {err['message']}") job_id = r.json()["job_id"] while True: job = requests.get(f"{BASE}/api/v1/job/{job_id}", headers=HEADERS).json() if job["status"] in ("completed", "failed"): break time.sleep(5)
Each accepted call counts as one API call. Polling or cancelling a job is not counted, and neither is a call refused for missing consent. Beyond the quota the API answers 429 monthly_quota_exceeded until the first day of the next month (UTC).
| Plan | API calls per month |
|---|---|
| API plan | 1,000 |
| Enterprise | Set by contract |
Short-term rate limits also apply per key (per minute and per day) and answer 429 rate_limit_exceeded with a retry delay. The fast verdict route is limited to 10 calls per hour per key.
Every error of the /api/v1 routes has the same shape. code is stable and meant for programs; message is for people; doc_url points here. The top-level code and message are copies kept for clients written against the former format, where error was a string.
{
"error": {
"code": "consent_required",
"message": "...",
"doc_url": "https://oravys.com/developers#errors"
},
"code": "consent_required",
"message": "..."
}
On POST /api/v1/analyze, an error that happens after the stream has started arrives in the final JSON line with HTTP 200, "success": false and the original status.
| Code | HTTP | Meaning |
|---|---|---|
| missing_api_key | 401 | No key in the headers. |
| invalid_api_key | 401 | Unknown, revoked or expired key. |
| insufficient_permissions | 403 | The key lacks the required scope. |
| consent_required | 400 | No consent attestation in the request. |
| missing_audio, empty_audio | 400 | No audio file, or an empty one. |
| invalid_audio, audio_decode_error | 400 | The file could not be read as audio. |
| audio_too_large | 413 | The file exceeds the size limit of the route. |
| job_not_found | 404 | Unknown job, or a job submitted with another key. |
| result_not_retrievable | 503 | The job finished but its result is not reachable yet. Retry shortly. |
| idempotency_conflict | 422 | Idempotency-Key reused with a different body. |
| monthly_quota_exceeded | 429 | Monthly API call quota reached. |
| rate_limit_exceeded | 429 | Too many calls in a short window. Retry after the indicated delay. |
| analysis_timeout | 504 | The synchronous analysis did not finish in time. Use the async route. |
| internal_error | 500 | Unexpected server error. |