Integrate emotional voice analysis into your product
Send audio, check analysis status, and download the normalized report from your own backend — the same intelligence the Voice Feeling portal uses, exposed as a REST API.
From an audio file to a report
Four calls cover the whole flow: create the analysis, check its status, and download the normalized report once it finishes.
Create an analysis by uploading the file
Send the audio as multipart/form-data. `consentObtained=true` is required: it confirms you have consent or a legal basis to analyze the recording.
curl https://voice-feeling.pages.dev/api/v1/analyses \ -H "Authorization: Bearer vf_live_xxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \ -H "Idempotency-Key: 5f3d2b7e-9c2e-4f6a-9c0e-1a2b3c4d5e6f" \ -F "file=@call.wav" \ -F "consentObtained=true" \ -F "businessContext=sales"Create an analysis from a URL
If the audio already lives on your infrastructure, send JSON with `audioUrl` instead of uploading the file. The same `consentObtained: true` is still required.
curl https://voice-feeling.pages.dev/api/v1/analyses \ -H "Authorization: Bearer vf_live_xxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \ -H "Idempotency-Key: 5f3d2b7e-9c2e-4f6a-9c0e-1a2b3c4d5e6f" \ -H "Content-Type: application/json" \ -d '{ "audioUrl": "https://cdn.example.com/calls/2026-09-03-0142.wav", "consentObtained": true, "businessContext": "service" }'Check the status
The analysis moves through several states until `completed` or `failed`. Poll with backoff, or register a webhook to be notified instead.
curl https://voice-feeling.pages.dev/api/v1/analyses/an_9f3c2e1a \ -H "Authorization: Bearer vf_live_xxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # { # "id": "an_9f3c2e1a", # "status": "completed", # "businessContext": "service", # "createdAt": "2026-09-03T14:02:11Z", # "completedAt": "2026-09-03T14:03:47Z", # "reportUrl": "/api/v1/analyses/an_9f3c2e1a/report" # }Download the normalized report
Only available once the analysis is `completed`. The report follows the `voicefeeling.report.v1` contract and never contains the provider's raw response.
curl https://voice-feeling.pages.dev/api/v1/analyses/an_9f3c2e1a/report \ -H "Authorization: Bearer vf_live_xxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Send an `Idempotency-Key` header on `POST /analyses` so network retries can't duplicate the analysis.
API keys and scopes
Authenticate every request with an API key in the Authorization header. Create and manage your keys from the portal, in the Developers section.
- Required header
Authorization: Bearer vf_live_<prefix>_<secret>- Key format
- vf_live_<prefix>_<secret>. The prefix identifies the key in logs; the secret is only shown once when it's created.
Available scopes
Each key declares the scopes it needs. A request missing the required scope gets `api_scope_missing` with the missing scope in the response.
analyses:readRead analyses, their status, and the normalized report.analyses:writeCreate analyses from an uploaded file or a URL.audios:writeUpload audio files as part of creating an analysis.webhooks:manageRegister, list, and delete webhook endpoints.usage:readRead the organization's usage against its limits.
Safe retries
Every delivery can repeat: if your network request fails without a clear response, resend it with the same `Idempotency-Key` header. If the key was already used with a different body, the API returns `idempotency_conflict` instead of creating a second analysis.
Event notifications
Instead of polling, register an HTTPS endpoint to receive events when an analysis finishes.
Available events
analysis.completedThe analysis finished and the normalized report is available.analysis.failedThe analysis ended in failure after exhausting retries.
Payload shape
Each delivery is a JSON POST with identification and signature headers.
POST https://your-domain.com/webhooks/voicefeeling
Content-Type: application/json
Voice-Feeling-Event: analysis.completed
Voice-Feeling-Signature: t=1756900931,v1=4b1f9e...c2a0
{
"event": "analysis.completed",
"analysisId": "an_9f3c2e1a",
"createdAt": "2026-09-03T14:03:47Z"
}Verify the signature
Compute the HMAC-SHA256 of the timestamp and body with your secret, and compare it to the received signature before trusting the payload. Reject deliveries whose timestamp is more than 5 minutes off from your clock.
const crypto = require("node:crypto");
function isValidSignature(header, rawBody, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(
header.split(",").map((part) => part.split("="))
);
const timestamp = Number(parts.t);
if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) {
return false;
}
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 || ""));
}Retries and disabling
Failed deliveries retry with exponential backoff for about an hour. An endpoint with 20 consecutive failures is disabled automatically; re-enable it by deleting and re-registering it from the portal.
Error codes
Every error response uses the same envelope: `{ "error": { "code", "message", "docs_url" } }`. Build your integration against `code`, not `message`, which can change language based on `Accept-Language`.
| CODE | HTTP | DESCRIPTION |
|---|---|---|
api_key_invalid | 401 | The key doesn't exist or doesn't match any registered key. |
api_key_expired | 401 | The key had an expiration date that has passed. |
api_key_revoked | 401 | The key was revoked from the portal. |
api_scope_missing | 403 | The key is missing the required scope; the response includes `required_scope`. |
api_rate_limited | 429 | The per-key request rate limit was exceeded; respect the `Retry-After` header. |
api_disabled | 503 | The public API is disabled in this environment. |
consent_required | 422 | The analysis creation request is missing `consentObtained: true`. |
audio_url_invalid | 422 | `audioUrl` isn't a valid, public https URL. |
audio_download_failed | 422 | The audio couldn't be downloaded from `audioUrl`. |
unsupported_audio_content | 415 | The file's content doesn't match a supported audio format. |
audio_too_large | 413 | The file exceeds the allowed size limit. |
monthly_analysis_quota_exceeded | 429 | The plan's monthly analysis quota was reached. |
daily_upload_quota_exceeded | 429 | The plan's daily upload quota was reached. |
storage_quota_exceeded | 413 | The organization's storage reached its limit. |
idempotency_conflict | 409 | The `Idempotency-Key` was already used with a different request body. |
analysis_not_found | 404 | No analysis with that id exists for this organization. |
report_requires_final | 409 | The analysis hasn't finished yet; the report isn't available. |
webhook_url_invalid | 422 | The webhook URL isn't a valid, public https URL. |
webhook_endpoint_not_found | 404 | No webhook endpoint with that id exists for this organization. |
webhook_limit_reached | 409 | The maximum number of webhook endpoints per organization was reached. |
Technical limits
Design your integration around these limits; exceeding them returns the matching error code, not a silent cutoff.
- Request rate
- 120 requests / 10 minutes per key
- Maximum audio size
- 50 MiB per file
- Supported formats
- WAV, MP3, M4A, OGG, FLAC, WebM, AAC, AIFF
Voice Feeling measures acoustic signals (energy, tension, pace) correlated with emotional states. They are not psychological facts, do not determine intent, and are never a truth-or-lie verdict. Any decision affecting a person must combine this with human context and your organization's policies.
