Errors
How Speechbase reports failures using RFC 7807 Problem Detail, and the error codes you should plan for.
Most 4xx and 5xx responses from api.speechbase.ai follow
RFC 7807 Problem Detail with content
type application/problem+json. The body always contains at least type,
title, and status; most also include a human-readable detail.
{
"type": "about:blank",
"title": "Unprocessable Entity",
"status": 422,
"detail": "voices.0.text: must not be empty"
}The one exception is content moderation blocks (422), which return a
richer JSON envelope (application/json, not application/problem+json) so
clients can branch on the moderation reason. See the row in the domain table
below.
Status codes you'll see
| Status | Meaning |
|---|---|
400 | Validation error. The request was malformed; detail names the offending field. |
401 | Missing, malformed, or revoked Speechbase API key. |
403 | The request is forbidden, including missing provider access, disabled provider access, or a cross-org access attempt. |
404 | The resource (voice, …) doesn't exist or belongs to another organisation. |
409 | Conflict — typically a duplicate name on a unique-constrained resource. |
422 | Unprocessable. Most commonly a content moderation block; see the table below. |
503 | Upstream provider is unavailable, or word-level timestamps couldn't be produced. |
204 | Success with no body. Returned by every DELETE. |
Domain-specific error codes
Some failures carry a stable machine-readable identifier in the response body in addition to the standard fields. Plan for these explicitly when you wire up retries and user-facing messages:
| Code | Status | What it means |
|---|---|---|
content_moderation_blocked | 422 | Text was rejected by moderation. Returned as application/json (not problem+json); body is { "error": { "code", "message", "reason": { "type", "detail", "confidence", "rule_name"? } } }. |
no_api_key | 403 | The provider you targeted has no BYOK credential stored for this org, and Managed Routing is not available for the request. |
provider_disabled | 403 | The provider has a key but is currently disabled in the dashboard. |
provider_unavailable | 503 | Upstream provider returned an error or was unreachable. |
timestamps_unavailable | 503 | A with-timestamps speech request couldn't produce alignments natively or via STT fallback. (Conversations may instead return audio with empty timestamps and a warnings entry.) |
error_fail_closed | 422 | Moderation evaluator errored out and the org is configured to fail-closed. Reported as a content_moderation_blocked body with reason.type: "error_fail_closed". |
Handling errors well
Always check the HTTP status before parsing — content type alone isn't enough,
since moderation blocks come back as application/json.
const res = await fetch("https://api.speechbase.ai/v1/audio/speech", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SPEECHBASE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
mode: "inline",
text,
voice: "alloy",
model: "openai/gpt-4o-mini-tts",
}),
});
if (!res.ok) {
const body = await res.json();
if (res.status === 422 && body?.error?.code === "content_moderation_blocked") {
// Surface a friendly message to the user; don't retry the same text.
} else if (res.status >= 500) {
// Backoff and retry.
} else {
throw new Error(`${body.title ?? body?.error?.code}: ${body.detail ?? body?.error?.message}`);
}
}A few conventions worth following:
- Don't retry 4xx. Validation, auth, and moderation failures will not pass on retry without changing the request.
- Backoff on 5xx and
provider_unavailable. Use jittered exponential backoff capped at ~30s. - Log the
typeandtitlefields, not the inbound text. Provider error bodies sometimes echo back the input — never store that in your own logs.
See Logging hygiene for what's safe to record.