TemboAI Voice API
TemboAI Voice turns short recordings into text through a project-scoped HTTP API. Upload a permissioned source, submit a transcription request, then read or explicitly reconcile that request. The Console at temboai.com/console manages organisations, projects, API keys, usage and budgets.
Languages and limits
| Code | Project language |
|---|---|
en | English |
pcm | Nigerian Pidgin |
yo | Yoruba |
ig | Igbo |
ha | Hausa |
A project can select one or more of these languages. Specify one selected language for each uploaded recording and use that same language for its transcription request. A single-language project may default the upload language; a multi-language project requires it. The hosted recognizer detects speech automatically: this code is metadata and validation, not a source-language hint sent to the provider. Supported metadata does not establish measured recognition quality. Review names, numbers, units, negation and time references before using transcripts.
Recordings must be 1–30 seconds, mono, and at most 4 MiB. Accepted formats are signed 16-bit PCM WAV at 16, 24 or 48 kHz, or validated single-stream Ogg/Opus. MP3, AAC, MP4 and WebM are not accepted uploads. See audio and permissions.
Create or activate a live project
- Sign in to Console and create or select your organisation.
- As an organisation owner or administrator, create a project and select its languages.
- Read the Ready for live transcription service agreement, then explicitly check I accept this service agreement and authorise processing by YarnGPT before creating the live project.
- For an existing empty project, read the same agreement and choose Activate live project. Activation is blocked for a nonempty or previously reviewed project that needs operator review; it does not overwrite retained evidence or an existing reviewed policy.
The released agreement is voice-service-20261030-v1, with policy SHA256 2661753f9a0ed5847f592e5d495aae74e86ea0523c0ee44cfdaea6c89a738269. Acceptance is explicit and records the immutable policy, accepting user, time and digest. Project activation is your choice; reading this guide does not accept the agreement.
Self-service projects have these ceilings:
| Control | Limit |
|---|---|
| Per recording | 4 MiB and 1–30 seconds |
| Total retained audio per project | 64 MiB, not 64 MiB per recording |
| TemboAI private audio access | 24 hours after ingestion |
| Concurrent requests | One |
| Operation allowance ceiling | 10,000 operations; grants no prepaid credits |
YarnGPT retains audio for 30 days and transcripts afterward. TemboAI's deletion does not remove those copies. The agreement grants no optional training permission.
Hosted processing and billing are enabled on the released service. Acceptance and activation create neither API keys nor credits automatically; an insufficient prepaid organisation balance blocks processing. Existing administrator-provisioned projects retain their own reviewed policy rather than silently inheriting the self-service limits.
Playground or external client
The signed-in Console Playground needs no API key: it uses your authenticated Console session. Select a permitted recording language, review the recording-level permission basis, and submit only when you choose to process that recording. For an external application, explicitly create a scoped API key and follow the quickstart.
A current organisation owner or administrator can issue an explicitly confirmed live key in Console. The secret is displayed once. Keep it on your server and send it as Authorization: Bearer <your existing live key>. Keys are project-bound and scoped:
| Scope | Allows |
|---|---|
voice:read | Read requests, list sources and create short-lived media links |
voice:write | Upload sources, submit requests, reconcile and cancel requests, update labels |
voice:delete | Withdraw a source |
Only owners/admins can issue keys. Membership changes, revoked keys and cross-project use can invalidate access. Console sessions authorise the Playground; browser cookies are not machine API authority on /api/v1/voice/ routes. tv_test_… keys are limited to development/test projects; a sample fixture is not a free hosted sandbox. Never put a key in frontend code, URLs or logs.
Local developer fixtures remain separate from hosted processing: development/test fixture mode supports the synthetic Hausa sample, not real recordings or five-language quality claims. Fixture identities and permission references must never be used as live evidence.
Start with the quickstart, then consult endpoints and errors, billing, and permissions and retention.
Current capability
Hosted requests support operation: 'transcribe'. The fixture-only interpret and render operations are not hosted translation or speech synthesis products. There is no public streaming ASR/TTS, arbitrary URL ingestion or WhatsApp webhook endpoint in this contract. A WhatsApp integration must securely obtain the media, validate/convert it to a supported format, obtain the required permission evidence, and use the same upload/request flow.
This API wraps Sails AI speech jobs; applications call TemboAI's endpoints rather than the provider's jobs API. TemboAI persists provider receipts and accounts for tenant permissions and reservations.
Quick Start
Use Node.js 22 or newer, a live project whose service agreement you have accepted, an existing live API key with voice:read and voice:write, and audio you are permitted to process. A later withdrawal also needs voice:delete. These commands can spend your organisation's credits when you run them against an activated project.
The signed-in Console Playground can transcribe without an API key. This guide is for external server clients; creating or activating a project does not create a key or grant credits.
Prepare permission evidence
For a self-service project activated under the current agreement, use the notice version and basis below. For an existing administrator-provisioned project, use its own approved notice version and basis instead. Create permission.json using your actual permission record:
{
"noticeVersion": "voice-service-20261030-v1",
"purpose": "voice-service",
"basis": "organisation service agreement; recording-level licence or speaker permission required",
"reference": "YOUR_PERMISSION_RECORD_REFERENCE",
"collectedAt": 0
}Replace collectedAt: 0 with the real time permission was collected, as Unix milliseconds. It must be in the past year and not in the future; do not fabricate a fresh timestamp for missing permission. reference must contain 8–250 characters. The exact same object must accompany both source upload and transcription; do not regenerate its timestamp between calls. A sample JSON object is not evidence that consent or another valid processing basis exists.
Configure your server process
Download quickstart.mjs into your application. Set these environment variables using your existing secret-management process; the example embeds no API key:
export TEMBO_BASE_URL='https://temboai.com'
export TEMBO_PROJECT_ID='YOUR_PROJECT_UUID'
export TEMBO_LANGUAGE='ha'
export TEMBO_AUDIO='/absolute/path/to/note.ogg'
export TEMBO_PERMISSION='/absolute/path/to/permission.json'
export TEMBO_STATE='./tembo-state.json'
# Supply TEMBO_API_KEY through your server's secret environment.TEMBO_LANGUAGE must be one of the languages selected for this project: en, pcm, yo, ig or ha.
node quickstart.mjs upload
node quickstart.mjs submit
node quickstart.mjs statusThe upload sends that language as a multipart field and returns a retained source ID. Submit returns the TemboAI request ID, status, output and any API-credit reservation. HTTP 200 does not imply transcription succeeded; inspect status, errorCode and apiCredits.
When the saved request is reconciliation-required, run one explicit reconciliation:
node quickstart.mjs pollThis command first reads the TemboAI request. If reconciliation is needed it makes one POST to /requests/:requestId/poll, which checks the already accepted provider job. It does not create another transcription. Repeat only through a bounded application-owned schedule with backoff, a finite attempt budget and an overall deadline. status only reads stored state; it never polls the provider.
When status is succeeded, the transcript is at output.transcript, with output.originalTranscript and provenance. Empty text can be valid for silence. On failed or cancelled, inspect errorCode. If reconciliation remains unresolved, keep the request identity and spend reservation and contact TemboAI rather than creating new keys or requests.
Persistence and retry behavior
The script saves the source-upload key, transcription request UUID and idempotency key, permission object, input fingerprint and API results in tembo-state.json. Treat the file as private and durable; exclude it from Git and use one state file per recording. It does not store your API key. Run only one process per state file. Production applications should replace local file storage with transactional durable storage and enforce the same input/key binding.
A failed network request triggers no automatic retry. After a lost transcription response, use status with the saved UUID first. If no request exists and you explicitly decide to retry, re-running submit uses the original payload and key. Do not remove the state file to generate a fresh request. Re-running upload uses the original upload key and rejects changed input; keep the audio and permission record unchanged.
To withdraw the uploaded recording with a voice:delete key:
node quickstart.mjs withdrawWithdrawal immediately excludes the source from processing and removes transcript access, while physical deletion may remain pending. See retention and withdrawal.
Complete client
The downloaded file is the exact source below. It uses native Node.js APIs and makes only the operation you request. The original audio is identified from its contents by the server; changing a filename or MIME header cannot make an unsupported recording valid.
import fs from 'node:fs/promises'
import { createHash, randomUUID } from 'node:crypto'
import { pathToFileURL } from 'node:url'
// Node.js 22+. This client makes only the explicitly requested HTTP operation.
export async function run(command, settings, fetchImpl = fetch) {
const {
baseUrl = 'https://temboai.com',
projectId,
apiKey,
audioPath,
permissionPath,
language,
statePath = 'tembo-state.json'
} = settings
if (!projectId || !apiKey)
throw new Error('Set TEMBO_PROJECT_ID and TEMBO_API_KEY')
if (!['upload', 'submit', 'status', 'poll', 'withdraw'].includes(command)) {
throw new Error('Use upload, submit, status, poll or withdraw')
}
const root = `${baseUrl.replace(/\/$/, '')}/api/v1/voice/projects/${projectId}`
async function save(state) {
await fs.writeFile(statePath, JSON.stringify(state, null, 2), {
mode: 0o600
})
}
async function request(path, options = {}) {
const response = await fetchImpl(root + path, {
...options,
headers: { Authorization: `Bearer ${apiKey}`, ...options.headers },
signal: AbortSignal.timeout(65000)
})
const data = await response.json()
if (!response.ok) {
throw Object.assign(new Error(data.error?.code || 'http_error'), {
status: response.status,
code: data.error?.code
})
}
return data
}
let state
try {
state = JSON.parse(await fs.readFile(statePath, 'utf8'))
} catch (error) {
if (error.code !== 'ENOENT') throw error
}
if (command === 'upload') {
if (
!audioPath ||
!permissionPath ||
!['en', 'pcm', 'yo', 'ig', 'ha'].includes(language)
) {
throw new Error('Set TEMBO_AUDIO, TEMBO_PERMISSION and TEMBO_LANGUAGE')
}
const audio = await fs.readFile(audioPath)
const permission = JSON.parse(await fs.readFile(permissionPath, 'utf8'))
const fingerprint = createHash('sha256')
.update(audio)
.update(JSON.stringify(permission))
.update(language)
.digest('hex')
if (
state &&
(state.fingerprint !== fingerprint ||
state.projectId !== projectId ||
state.root !== root)
) {
throw new Error(
'Existing state belongs to different input; use a separate state file'
)
}
const existingState = Boolean(state)
state ||= {
root,
projectId,
language,
permission,
fingerprint,
uploadKey: randomUUID(),
requestId: randomUUID(),
requestKey: randomUUID()
}
if (!state.source) {
// Save the stable input binding and both submission identities before HTTP.
await fs.writeFile(statePath, JSON.stringify(state, null, 2), {
flag: existingState ? 'w' : 'wx',
mode: 0o600
})
const form = new FormData()
form.append('idempotencyKey', state.uploadKey)
form.append('permission', JSON.stringify(state.permission))
form.append('language', state.language)
form.append('audio', new Blob([audio]), 'note.audio')
state.source = await request('/sources', { method: 'POST', body: form })
await save(state)
}
return state.source
}
if (
!state ||
state.root !== root ||
state.projectId !== projectId ||
!state.source
) {
throw new Error('Upload first, using this project and state file')
}
const requestPath = `/requests/${state.requestId}`
let result
if (command === 'submit') {
if (state.source.status !== 'retained')
throw new Error('Source must be retained before submission')
if (state.request)
throw new Error('Request already saved; use status or poll')
// Repeating this explicit command after a lost response uses the same body/key.
result = await request('/requests', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
requestId: state.requestId,
idempotencyKey: state.requestKey,
operation: 'transcribe',
language: state.language,
sourceId: state.source.id,
servicePermission: state.permission
})
})
} else if (command === 'withdraw') {
result = await request(`/sources/${state.source.id}`, { method: 'DELETE' })
state.source = result
await save(state)
return result
} else {
result = await request(requestPath)
if (command === 'poll' && result.status === 'reconciliation-required') {
// Exactly one explicit provider reconciliation. No automatic polling loop.
result = await request(requestPath + '/poll', { method: 'POST' })
}
}
state.request = result
await save(state)
return result
}
if (
process.argv[1] &&
import.meta.url === pathToFileURL(process.argv[1]).href
) {
try {
const result = await run(process.argv[2], {
baseUrl: process.env.TEMBO_BASE_URL,
projectId: process.env.TEMBO_PROJECT_ID,
apiKey: process.env.TEMBO_API_KEY,
audioPath: process.env.TEMBO_AUDIO,
permissionPath: process.env.TEMBO_PERMISSION,
language: process.env.TEMBO_LANGUAGE,
statePath: process.env.TEMBO_STATE || 'tembo-state.json'
})
console.log(JSON.stringify(result, null, 2))
} catch (error) {
console.error(error.code || error.message)
console.error(
'Keep the state file. Inspect status before retrying; no automatic POST retry was made.'
)
process.exitCode = 1
}
}Endpoints and errors
Base URL: https://temboai.com/api/v1/voice/projects/:projectId. Use an existing project-scoped bearer key. Sources and requests from another project cannot be used through your project's routes.
Sources
| Method and path | Scope | Request / response |
|---|---|---|
POST /sources | voice:write | Multipart audio, idempotencyKey, JSON permission, recording language; optional JSON trainingEvidence |
GET /sources | voice:read | { sources: [...] }, latest 100 sources |
GET /sources/:sourceId/media-link | voice:read | { url, expires }; relative signed URL, expires in 60 seconds |
PATCH /sources/:sourceId/labels | voice:write | Correct labels with provenance; not needed to transcribe |
DELETE /sources/:sourceId | voice:delete | Source status after withdrawal/deletion |
An uploaded source returns id, language, eligibility, status, durationSeconds, byteSize, mimeType, sha256, retentionUntil and createdAt. Submit only a source with status: 'retained'. Without optional training evidence its eligibility is service-only. Never use client-supplied recording URLs in place of source upload.
permission has noticeVersion, purpose: 'voice-service', basis, reference and collectedAt. It must match the approved project notice. See permission rules.
Requests
| Method and path | Scope | Behavior |
|---|---|---|
POST /requests | voice:write | Submit or replay an idempotent transcription |
GET /requests/:requestId | voice:read | Read stored public request state; no provider call |
POST /requests/:requestId/poll | voice:write | Explicitly reconcile an existing held provider job |
DELETE /requests/:requestId | voice:write | Request cancellation; not guaranteed provider cancellation or refund |
Submission is JSON with these fields:
| Field | Contract |
|---|---|
requestId | Persisted UUID-shaped identifier: 36 lowercase hex/hyphen characters; use a proper UUID |
idempotencyKey | 8–100 ASCII letters, digits, underscores or hyphens |
operation | transcribe for hosted processing |
language | One project-selected language matching this source: en, pcm, yo, ig, ha |
sourceId | ID of a retained, unexpired, permissioned source in this project |
servicePermission | Exact permission evidence bound to the source |
provider | Optional; omit it to use the configured provider consistently |
Idempotency keys belong in the request body, not an Idempotency-Key HTTP header. Source upload and request submission have separate keys, both scoped to the project. Reusing a source key with different bytes, permission, recording language or training evidence returns idempotency_conflict. A request key replays the existing request with replay: true when normalized input matches; conflicting input returns 409. Persist the original requestId, key and body even though request replay is keyed by the idempotency key. Keep optional provider selection consistent across retries.
A request response contains id, operation, status, output, errorCode, inputProvenance, outputProvenance, createdAt, trainingEligibility and apiCredits. Output is null while unavailable. A successful transcript exposes output.transcript, originalTranscript, uncertainty, criticalChecks and provenance. The response does not expose the private provider receipt or signed provider output URL.
Status and recovery
| Status | Caller action |
|---|---|
running | Read stored status later; do not submit a second request |
reconciliation-required | Keep the reservation; explicitly POST one /poll when appropriate |
succeeded | Read transcript and settled usage; inspect transcript quality |
failed | Inspect errorCode and billing state |
cancelled | Inspect billing state; do not assume provider cancellation |
An accepted provider job that is queued/processing, or an uncertain submission, is represented as reconciliation-required. Polling is accepted only for a held request with a stored provider receipt; otherwise it returns request_not_reconcilable. A receipt with no accepted provider ID remains held and cannot be looked up upstream. There is no client-visible provider receipt to replace or reconstruct. Contact TemboAI with the project and request IDs when recovery cannot progress.
Deleting a request with a provider receipt retains reconciliation rather than pretending that provider work stopped. An output can be hidden with source_unavailable after source expiry or withdrawal even if the request itself succeeded. Stop automatic polling on terminal state and bound the number of nonterminal attempts. A server request can wait for its configured provider timeout (15 seconds by default, at most 60 seconds); choose a client timeout that accommodates the approved deployment setting. A timeout is not proof of no charge.
Errors
Voice application errors use {"error":{"code":"..."}} and an HTTP status. Sails input-validation or upload-limit errors may use framework error envelopes; check the HTTP status even when no error.code exists. Provider error bodies and credentials are not forwarded.
| Status | Common codes / meaning |
|---|---|
| 400 | invalid_idempotency_key, invalid_request_id, unsupported_language, recording_language_required, unsupported_operation, invalid_pcm_wav, invalid_ogg_opus, audio_duration_limit, invalid_permission_json |
| 401 | bearer_key_required, invalid_key, test_key_not_allowed |
| 402 | insufficient_api_credits |
| 403 | insufficient_scope, service_permission_required, source_permission_mismatch, source_permission_changed, provider_processing_not_approved |
| 404 | project_not_found, request_not_found, source_not_found, source_unavailable, media_link_invalid |
| 409 | idempotency_conflict, project_paused, source_language_mismatch, request_not_reconcilable, partner_agent_operation_required |
| 429 | concurrency_limit, trial_limit_reached, storage_limit_reached |
| 503 | live_access_not_configured, provider_not_configured, api_credit_rate_not_approved, private_storage_not_configured |
| 500 | voice_internal_error or a framework/upload failure |
Do not blindly retry a chargeable submission after a transport error or 5xx. Preserve input and identity, read stored state first, and retry only the identical operation when explicitly appropriate. Back off on capacity errors and keep a deadline. This contract does not guarantee a Retry-After header.
Console setup endpoints are separate
POST /console/projects creates a project through an authenticated owner/admin session with selected languages and explicit serviceAgreement: { accepted: true, version: 'voice-service-20261030-v1' }. POST /console/projects/:projectId/activate applies the same accepted agreement only to an eligible empty project. These are Console actions with browser security checks, not bearer-key machine endpoints. Use the UI to review and accept the agreement; the quickstart never activates a project, creates a key, buys credits or accepts terms.
Missing or stale acceptance returns service_agreement_required (403); a nonempty or already-reviewed nonlive project returns existing_project_review_required (409). Acceptance replays do not rewrite an already recorded live-project policy.
Credits and billing
TemboAI API credits belong to an organisation and are separate from provider credits and your application's own prices. The configured face value is 20 kobo per credit: 10,000 purchased credits cost ₦2,000. They are not transferable or cashable out. A new organisation's promotional credit grant is a separate ledger bucket, not a purchased balance.
The current STT rate card covers en, pcm, yo, ig and ha at 5 credits per started audio second. The measured recording duration is rounded up, independently for each recording:
quoted credits = ceil(duration in milliseconds / 1000) × 5| Validated duration | Credits | Face value |
|---|---|---|
| 1 second | 5 | ₦1 |
| 5 seconds | 25 | ₦5 |
| 5.001 seconds | 30 | ₦6 |
| 15 seconds | 75 | ₦15 |
| 30 seconds | 150 | ₦30 |
Durations above 30 seconds are rejected. Uploaded media is structurally validated; a forged filename or duration field cannot reduce the measured duration. The current rate-card version is stt-ngn-20261029-v2. Older Hausa-only quote snapshots retain their original rate version.
Quote, reservation and settlement
apiCredits on a request is either null when there is no reservation, or an object containing:
{
"quoted": 30,
"status": "reserved",
"rateVersion": "stt-ngn-20261029-v2",
"measure": "started_audio_second",
"quantity": 6
}This example represents a six-started-second reservation. Credits are reserved before a hosted submission; promotional credits are used first, then purchased credits. A successful request settles its original quote once. Safely known uncharged failures can release the reservation. Unknown or potentially charged provider outcomes remain held for reconciliation. A failed or cancelled request alone does not prove a release or refund: inspect apiCredits.status (reserved, settled or released).
Do not calculate customer charges from upstream creditsReserved or creditsCharged. Those are provider metering units, not the TemboAI tariff. Reservations, organisation balance, project allowance, concurrency and storage limits are separate controls.
Funding and availability
In Console, open Usage → Buy credits as an organisation owner or admin. Choose Other payment methods or your available Cowrie balance, then review the server-issued quote before confirming. Purchases are in NGN; one Cowrie buys five API credits. For other payment methods, choose your payment method at checkout. The hosted checkout offers the methods eligible for the account, currency and amount; TemboAI does not restrict it to bank transfers. USD checkout and automatic top-ups are not currently available. External payments fulfill credits only after a verified payment webhook; a browser redirect is not a credit receipt. Use Refresh status to check a pending payment. An interrupted checkout can recover the same purchase; do not start another payment when its outcome is unresolved. Cowrie purchases require funded eligible Cowries; do not treat unrelated contribution earnings as spendable API credit. Saved auto-topup settings are preferences only: automatic payments are unavailable and saving them creates no payment mandate.
Hosted processing and billing are enabled on the released service. Self-service activation creates no key, payment or credit grant. Its 10,000-operation allowance ceiling is separate from prepaid credits: it is not a balance of 10,000 credits and does not pay for transcription. The project also has one concurrent request and a 64 MiB total retained-audio ceiling. Check Console readiness and project activation. Metadata language support and published pricing do not establish measured recognition quality for every language.
Audio, permissions and retention
Supported recordings
Uploads use multipart form field audio, with idempotencyKey, recording language and JSON permission fields. The recording language must be selected for the project; multi-language projects require an explicit upload language. Let your HTTP client set the multipart boundary. TemboAI validates actual bytes rather than trusting the uploaded MIME type or filename. Maximum size is 4 MiB and duration is 1–30 seconds.
| Container | Required encoding |
|---|---|
| WAV | RIFF/WAVE, PCM format 1, signed 16-bit little-endian, one channel, 16000/24000/48000 Hz |
| Ogg | One Opus stream, mono, valid headers, packet durations, sequence numbers, CRCs and end-of-stream granule; 48 kHz timeline |
Stereo, chained Ogg streams, malformed/truncated containers, MP3, AAC, MP4 and WebM are not accepted. Browser recordings may need conversion. WhatsApp Ogg/Opus recordings must still pass these limits; a WhatsApp MIME label is not sufficient.
For an existing recording you are permitted to process, this local conversion produces mono 16 kHz PCM WAV. It does not upload or call a provider:
ffmpeg -i input.ogg -ac 1 -ar 16000 -c:a pcm_s16le output.wavCheck duration and size before upload; this command does not trim long recordings. Obtain appropriate permission before processing or converting the source.
Service permission
Self-service activation records the accepted immutable voice-service-20261030-v1 service agreement. It authorises the organisation's use of transcription through YarnGPT only for recordings covered by a valid licence or the speaker's permission. The agreement is not itself that recording-level permission. It grants no optional training rights.
The project's approved service notice includes voice-service, the applicable basis and provider-processing notice for yarngpt.ai. For the self-service agreement the basis is exactly organisation service agreement; recording-level licence or speaker permission required. Your upload evidence must identify the actual licence or permission record. Existing administrator-provisioned projects keep their own approved notice and retention policy. Your upload evidence must contain:
noticeVersionequal to the current project notice version;purpose: 'voice-service';basisequal to the approved notice basis;reference, 8–250 characters, identifying the actual permission record; andcollectedAt, an integer Unix millisecond timestamp in the past year, not in the future.
Reuse that exact evidence object as servicePermission when submitting transcription. A changed source notice/basis, withdrawn or expired source, different project, or a recording language outside the project selection prevents processing. Store your permission record securely; the API verifies its binding and shape, not whether your underlying legal basis was actually obtained.
Training is a separate choice
Ordinary upload without trainingEvidence is service-only. Service permission does not grant model-training permission. Optional training evidence enters pending-review, not immediate eligibility. Internal review requires separate human-voice-training purpose, optional choice, source license, withdrawal reference and supporting evidence. Do not add fabricated training evidence to make an upload pass.
Provider output has trainingEligibility.eligible: false with reason provider_output_not_cleared. A transcript, language choice or provider account is not a training license.
Retention and withdrawal
For self-service projects, TemboAI's private audio access expires 24 hours after ingestion. retentionUntil records the expiry. Expired sources cannot be processed or accessed through media links. Physical deletion is scheduled in bounded batches; failed deletions are retried. The 64 MiB total project storage quota is released only after deletion succeeds, so expiry or a pending deletion does not immediately create space. Reserved uploads count toward the quota too.
YarnGPT retains audio for 30 days; transcripts remain with the provider afterward. TemboAI's deletion schedule and withdrawal do not remove the provider's copies. Read this disclosure before accepting the agreement and before submitting a recording. Do not promise provider erasure or a particular physical-deletion time from TemboAI's expiry timestamp.
Previously administrator-provisioned projects retain their reviewed retention settings; do not infer a change to their policy from the new self-service flow.
Withdraw with DELETE /sources/:sourceId and a voice:delete key. The source is immediately excluded, its transcript labels cleared and associated public request output redacted. Object removal may still be deletion-pending if storage fails; repeated deletion remains safe. Physical deletion becomes deleted only after confirmed removal. This does not assert deletion of accounting/provenance records or data already processed by an external provider.
Authenticated callers with voice:read can request /sources/:sourceId/media-link. It returns a relative signed URL and expires timestamp valid for 60 seconds. Resolve it against the TemboAI origin, treat the URL as private bearer access, and never log or publish it. Expiry or withdrawal invalidates access even if you still hold an earlier URL.