Documentation
Error Handling Learn how to handle errors from SM-AI-MODELS APIs.
Every response carries an X-Request-ID header. Quote it when contacting support.
Error Response Formats
The API returns three distinct error shapes depending on the failure type. Always switch on HTTP status first; the body tells you why .
1. Standard error — 400, 404, 500, 503, 504
{
"error" : "http_error" ,
"detail" : "Text exceeds maximum length of 51,200 characters" ,
"code" : "HTTP_400"
}
Field Type Notes errorstring http_error for HTTP errors, internal_error for unhandled exceptions.detailstring Human-readable message. Do not parse — it's not stable. codestring HTTP_<status> (e.g. HTTP_503) or INTERNAL_ERROR. Stable.
2. Authentication / rate-limit error — 401, 429
{
"error" : "unauthorized" ,
"detail" : "X-API-Key header is required"
}
Field Type Notes errorstring unauthorized (401) or rate_limit_exceeded (429).detailstring Human-readable message.
429 responses include a Retry-After: <seconds> header (default 60).
401 responses include WWW-Authenticate: Bearer.
3. Validation error — 422
Returned by FastAPI when the request body or form-data fails schema validation (e.g. wrong type, missing required field).
{
"error" : "validation_error" ,
"detail" : "Request validation failed" ,
"errors" : [
{ "field" : "body.input" , "message" : "field required" },
{ "field" : "body.speed" , "message" : "value is not a valid float" }
]
}
HTTP Status Codes
Status Meaning Body shape Action 200Success — Process response 400Bad Request Standard Check input values 401Missing / invalid API key Auth Send a valid X-API-Key 422Validation failure Validation Fix the offending fields 429Rate limit exceeded Auth Wait Retry-After seconds, then retry 500Internal error Standard Retry with backoff; contact support if persistent 503Engine not ready / queue full Standard Retry after a few seconds 504Queue timeout (GPU saturation) Standard Retry with backoff
Common Failure Causes
TTS (POST /v1/tts/audio/speech)
Status detail (example)Likely cause 400Text cannot be emptyMissing or empty input. 400Text exceeds maximum length of 51,200 charactersText too long. Split it. 400Invalid format. Allowed: aac, flac, mp3, opus, pcm, wavBad response_format. 400Text contains invalid contentInput matched a blocked injection pattern. 503TTS engine not initializedEngine still warming up — retry. 503Service busy, please try again laterGPU queue full. Honour Retry-After. 504Request timeout waiting for GPUQueue waited too long. Retry with backoff.
speed outside 0.5 – 2.0 is clamped silently (not rejected). It will not produce a 400.
ASR (POST /v1/asr/audio/transcriptions, POST /v1/asr/transcribe)
Status detail (example)Likely cause 400No selected fileThe file part is missing. 400Invalid file typeExtension not in flac, mp3, mp4, mpeg, mpga, m4a, ogg, wav, webm. 400Couldn't transcribe the file: ...Corrupt or unreadable audio. 503ASR engine not initializedEngine still warming up — retry. 503Service busy, try again laterGPU queue full. 504Request timeoutQueue timeout.
Retry Strategy
For transient errors (500, 503), implement exponential backoff:
import time
import requests
def request_with_retry (url, data, max_retries = 3 ):
for attempt in range (max_retries):
try :
response = requests.post(url, json = data, timeout = 30 )
if response.status_code == 200 :
return response
if response.status_code in [ 500 , 503 ]:
# Retry with exponential backoff
wait_time = ( 2 ** attempt) + 1
print ( f "Retry { attempt + 1} / { max_retries } in { wait_time } s" )
time.sleep(wait_time)
continue
# Don't retry client errors
response.raise_for_status()
except requests.Timeout:
if attempt < max_retries - 1 :
continue
raise
raise Exception ( "Max retries exceeded" )
Validation
Validate inputs before making requests:
def validate_tts_request (text, voice, format, speed):
errors = []
if not text or not text.strip():
errors.append( "Input text is required" )
if voice not in [ 'Yara' , 'Nouf' , 'Atheer' , 'Yara_en' ]:
errors.append( f "Invalid voice: { voice } " )
if format not in [ 'mp3' , 'wav' , 'opus' , 'flac' , 'pcm' , 'aac' ]:
errors.append( f "Invalid format: {format} " )
if not ( 0.5 <= speed <= 2.0 ):
errors.append( f "Speed must be between 0.5 and 2.0" )
return errors
Logging
Log errors for debugging:
import logging
logging.basicConfig( level = logging. INFO )
logger = logging.getLogger( __name__ )
def generate_speech_with_logging (text, voice = "Yara" ):
try :
response = requests.post(
"https://api.withsm.ai/v1/tts/audio/speech" ,
headers = { "X-API-Key" : "YOUR_API_KEY" },
json = { "input" : text, "voice" : voice}
)
if response.status_code != 200 :
error = response.json()
logger.error( f "TTS error: { error } " )
return None
logger.info( f "Generated speech for {len (text) } chars" )
return response.content
except Exception as e:
logger.exception( f "TTS request failed: { e } " )
return None
Health Checks
Check service health before making requests:
BASE_URL = "https://api.withsm.ai"
def ensure_service_healthy (service: str , timeout: int = 5 ) -> bool :
"""service is 'tts' or 'asr'."""
try :
response = requests.get(
f " {BASE_URL} /v1/ { service } /health" ,
timeout = timeout
)
return response.json().get( "status" ) == "healthy"
except Exception :
return False
# Usage
if ensure_service_healthy( "tts" ):
audio = generate_speech( "مرحباً" )
else :
print ( "TTS service not available" )
API Limits
For detailed information about API limits and constraints, see the API Limits documentation .
Key Limits
TTS speed range: 0.5 to 2.0 (values outside the range are clamped, not rejected)
TTS voices: Yara, Nouf, Atheer, Yara_en
TTS formats: mp3, wav, opus, flac, pcm, aac
TTS max input: 51,200 characters per request
ASR formats: flac, mp3, mp4, mpeg, mpga, m4a, ogg, wav, webm
ASR max file size: 100 MB
Recommended audio duration: Under 30 seconds for optimal performance
Complete Error Handling Example
import requests
import logging
from time import sleep
logging.basicConfig( level = logging. INFO )
logger = logging.getLogger( __name__ )
class SMAIClient :
def __init__ (self, base_url = "https://api.withsm.ai" , api_key = "YOUR_API_KEY" ):
self .base_url = base_url
self .headers = { "X-API-Key" : api_key}
def generate_speech (self, text, voice = "Yara" , format = "mp3" , speed = 1.0 , max_retries = 3 ):
"""Generate speech with comprehensive error handling."""
# Validate inputs
if not text or not text.strip():
raise ValueError ( "Input text is required" )
if voice not in [ 'Yara' , 'Nouf' , 'Atheer' , 'Yara_en' ]:
raise ValueError ( f "Invalid voice: { voice } . Use Yara, Nouf, Atheer, or Yara_en" )
if format not in [ 'mp3' , 'wav' , 'opus' , 'flac' ]:
raise ValueError ( f "Invalid format: {format} " )
if not ( 0.5 <= speed <= 2.0 ):
raise ValueError ( f "Speed must be between 0.5 and 2.0, got { speed } " )
# Make request with retry
for attempt in range (max_retries):
try :
response = requests.post(
f " {self .base_url } /v1/tts/audio/speech" ,
headers = self .headers,
json = {
"input" : text,
"voice" : voice,
"response_format" : format ,
"speed" : speed
},
timeout = 30
)
if response.status_code == 200 :
logger.info( f "Speech generated successfully ( {len (text) } chars)" )
return response.content
# Handle server errors with retry
if response.status_code in [ 500 , 503 ]:
wait_time = ( 2 ** attempt) + 1
logger.warning( f "Server error, retrying in { wait_time } s..." )
sleep(wait_time)
continue
# Handle client errors
error_data = response.json()
error_msg = error_data.get( "error" , {}).get( "message" , "Unknown error" )
logger.error( f "TTS error ( { response.status_code } ): { error_msg } " )
raise Exception ( f "TTS failed: { error_msg } " )
except requests.Timeout:
if attempt < max_retries - 1 :
logger.warning( f "Request timeout, retrying..." )
continue
raise Exception ( "Request timed out after retries" )
except requests.ConnectionError:
logger.error( "Cannot connect to TTS service" )
raise Exception ( "TTS service unavailable" )
raise Exception ( "Max retries exceeded" )
def transcribe (self, audio_path, max_retries = 3 ):
"""Transcribe audio with comprehensive error handling."""
import os
# Validate file
if not os.path.exists(audio_path):
raise FileNotFoundError ( f "Audio file not found: { audio_path } " )
file_ext = os.path.splitext(audio_path)[ 1 ].lower()
if file_ext not in [ '.flac' , '.mp3' , '.wav' , '.ogg' , '.webm' ]:
raise ValueError ( f "Unsupported format: { file_ext } " )
# Make request with retry
for attempt in range (max_retries):
try :
with open (audio_path, "rb" ) as f:
response = requests.post(
f " {self .base_url } /v1/asr/audio/transcriptions" ,
headers = self .headers,
files = { "file" : f},
timeout = 60
)
if response.status_code == 200 :
text = response.json()[ "text" ]
logger.info( f "Transcription successful: {len (text) } chars" )
return text
# Handle server errors with retry
if response.status_code in [ 500 , 503 ]:
wait_time = ( 2 ** attempt) + 1
logger.warning( f "Server error, retrying in { wait_time } s..." )
sleep(wait_time)
continue
# Handle client errors
error_data = response.json()
error_msg = error_data.get( "error" , {}).get( "message" , "Unknown error" )
logger.error( f "ASR error ( { response.status_code } ): { error_msg } " )
raise Exception ( f "Transcription failed: { error_msg } " )
except requests.Timeout:
if attempt < max_retries - 1 :
logger.warning( f "Request timeout, retrying..." )
continue
raise Exception ( "Request timed out after retries" )
except requests.ConnectionError:
logger.error( "Cannot connect to ASR service" )
raise Exception ( "ASR service unavailable" )
raise Exception ( "Max retries exceeded" )
# Usage
client = SMAIClient()
try :
audio = client.generate_speech( "مرحباً بكم" , voice = "Yara" )
with open ( "output.mp3" , "wb" ) as f:
f.write(audio)
print ( "✓ Speech generated" )
except Exception as e:
print ( f "✗ Failed: { e } " )
try :
text = client.transcribe( "recording.wav" )
print ( f "✓ Transcribed: { text } " )
except Exception as e:
print ( f "✗ Failed: { e } " )
Next Steps
Last modified on May 4, 2026