REST API
Errors & Rate Limits

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

CodeNameWhen
400Bad RequestMissing required field, invalid JSON, invalid ID format
401UnauthorizedMissing Authorization header, invalid key, revoked key
403ForbiddentemplateId not found or not accessible with this key
404Not FoundProject not found or not accessible with this key
422UnprocessableNo template available for the requested project type
429Too Many RequestsRate limit exceeded (see below)
500Internal Server ErrorUnexpected server error
502Bad GatewayFailed 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.

EndpointRate-limited
POST /api/projects✅ Yes
GET /api/projects/:id/statusNo
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: 30

Handling 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-limited

Polling 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: completed or failed — 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.