Errors & Rate Limits
Error Response Format
All error responses use the same JSON structure:
{
"error": "Human-readable error message",
"details": "Optional additional context"
}The details field is included for server errors (5xx) when extra diagnostic
information is available. Never display details to end users.
HTTP Status Codes
| Code | Name | When |
|---|---|---|
400 | Bad Request | Missing required field, invalid JSON, invalid ID format |
401 | Unauthorized | Missing Authorization header, invalid key, revoked key |
403 | Forbidden | templateId not found or not accessible with this key |
404 | Not Found | Project not found or not accessible with this key |
422 | Unprocessable | No template available for the requested project type |
429 | Too Many Requests | Rate limit exceeded (see below) |
500 | Internal Server Error | Unexpected server error |
502 | Bad Gateway | Failed to download audio from audioUrl (upstream error) |
Common Errors
400 — Missing required field:
{ "error": "Missing required field: title" }401 — Invalid API key:
{ "error": "Invalid API key" }422 — No template available:
{
"error": "No template available for this project type. Provide a templateId or configure a system default."
}Call GET /api/templates to see available templates.
502 — Audio download failed:
{
"error": "Failed to download audio from the provided URL",
"details": "Failed to fetch source URL (status 403): https://..."
}Ensure your presigned URL is valid and has not expired. Presigned URLs must remain valid for at least 5 minutes after the API call.
Rate Limits
Most endpoints use a sliding window rate limit of 100 requests per minute per API key.
| Endpoint | Rate-limited |
|---|---|
POST /api/projects | ✅ Yes |
GET /api/projects/:id/status | ❌ No |
GET /api/templates | ✅ Yes |
GET /api/projects/:id/status is intentionally not rate-limited. You can poll it
as frequently as needed, though we recommend at most once every 5–10 seconds.
Rate Limit Response
When you exceed the rate limit, you receive a 429 response:
{
"error": "Rate limit exceeded",
"retryAfterMs": 30000
}The response also includes the Retry-After header (in seconds):
HTTP/1.1 429 Too Many Requests
Retry-After: 30Handling Rate Limits
import time
import requests
def api_call_with_retry(url, **kwargs):
"""Make an API call, retrying once on rate limit."""
for attempt in range(2):
resp = requests.request(**kwargs, url=url)
if resp.status_code == 429:
data = resp.json()
retry_after_s = data.get("retryAfterMs", 60000) / 1000
print(f"Rate limited. Waiting {retry_after_s:.0f}s...")
time.sleep(retry_after_s)
continue
return resp
return resp # Return last response even if still rate-limitedPolling Guidance
When polling GET /api/projects/:id/status:
- Recommended interval: 10–30 seconds
- Minimum interval: 5 seconds (not enforced, but be considerate)
- Typical job duration: 3–15 minutes (varies by audio length)
- Terminal states:
completedorfailed— stop polling when you reach either
import time
def wait_for_completion(project_id: str, poll_interval: int = 15) -> dict:
"""Poll until the project reaches a terminal state."""
for _ in range(200): # ~50 minutes max
resp = requests.get(
f"https://app.heydonna.chat/api/projects/{project_id}/status",
headers={"Authorization": f"Bearer {API_KEY}"},
)
resp.raise_for_status()
status = resp.json()
if status["status"] in ("completed", "failed"):
return status
time.sleep(poll_interval)
raise TimeoutError(f"Project {project_id} did not complete within timeout")Idempotency
The POST /api/projects endpoint is not idempotent — each call creates a new project.
Use externalRef to correlate projects with your own order system and detect duplicates
on your side before submitting.