Error Codes & HTTP Status Reference

Every error response is JSON with this structure:

{
  "error": {
    "code": "error_code",
    "message": "Human-readable message"
  }
}

Billing: Errors are never billed (failures and cache hits have zero cost). Only successful responses (status=200, cache miss) are billable.


400 — Bad Request (invalid_params)

The request parameters are invalid.

Billed: No

Causes:

  • title is missing or exceeds 200 characters
  • template is not one of: basic, gradient, blog, product
  • theme is not light or dark
  • w or h are outside 100–2400 or 100–1260 range
  • format is not png, webp, avif, or svg (for /og) or png, webp, jpeg (for /screenshot)
  • expires is not a valid unix timestamp
  • Required parameter is missing (e.g., url for /screenshot)

Example response:

{
  "error": {
    "code": "invalid_params",
    "message": "title must be between 1 and 200 characters"
  }
}

How to fix: Validate all parameters on the client before sending. Check the API reference for allowed values and limits.


401 — Unauthorized (invalid_key)

The API key is missing, malformed, or revoked.

Billed: No

Causes:

  • Authorization: Bearer ... header is missing or malformed (must be Bearer sk_live_...)
  • The key does not exist (typo, never issued)
  • The key has been revoked with POST /v1/keys

Example response:

{
  "error": {
    "code": "invalid_key",
    "message": "API key not found or revoked"
  }
}

How to fix: Check that your Authorization header is exactly Bearer sk_live_<40 hex chars>. If the key was lost, generate a new one.


402 — Payment Required (quota_exceeded)

Free tier render quota exceeded.

Billed: No

Causes:

  • Free tier account has exceeded 500 renders in the current calendar month
  • Upgrade to PAYG to lift the limit

Example response:

{
  "error": {
    "code": "quota_exceeded",
    "message": "Free tier quota of 500 renders/month exceeded"
  }
}

How to fix: Upgrade to pay-as-you-go in your dashboard, or wait until the next month.


403 — Forbidden (invalid_signature or expired)

The request signature is invalid or the signature has expired.

Billed: No

Causes (invalid_signature):

  • sig parameter does not match the expected HMAC-SHA256
  • Parameters were modified after signing
  • Using an old or wrong SIGNING_SECRET
  • URL encoding mismatch during signing vs. transmission

Causes (expired):

  • expires timestamp is in the past
  • Current server time > expires unix seconds

Example response:

{
  "error": {
    "code": "invalid_signature",
    "message": "Signature verification failed"
  }
}

How to fix:

  1. Double-check SIGNING_SECRET — it must match the key shown in your dashboard
  2. Verify the signing algorithm: sort params alphabetically, URL-encode, join with &, then HMAC-SHA256
  3. If expires is set, ensure it's a future unix timestamp (e.g., Math.floor(Date.now() / 1000) + 3600 for 1 hour from now)
  4. Check that no URL parameters were modified after signing

Use the Quickstart signing example as a reference.


422 — Unprocessable Entity (render_failed)

The render engine encountered an error.

Billed: No (failures are never billed)

Causes:

  • Invalid CSS in the template (Satori only supports flexbox)
  • Invalid template HTML structure
  • Font loading failed (rare)
  • Out-of-memory (very large image dimensions with complex CSS)

Example response:

{
  "error": {
    "code": "render_failed",
    "message": "Satori render failed: unsupported CSS property 'grid'"
  }
}

How to fix:

  1. Use the Playground to test your template live
  2. Ensure CSS uses flexbox only (no grid, no calc(), no custom properties)
  3. Simplify the template — remove complex CSS, animations, or remote images
  4. Check that all fonts are bundled or passed as base64 data URIs

See OG Reference — Satori Constraints.


429 — Too Many Requests (rate_limited)

Rate limit exceeded.

Billed: No

Limits:

  • Free tier: 60 requests/minute per IP or API key
  • Paid tier (PAYG): 600 requests/minute per API key

Response includes Retry-After header with seconds to wait.

Example response:

{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded: 60 requests per minute"
  }
}

Headers:

Retry-After: 30

How to fix:

  1. Wait the number of seconds in Retry-After
  2. Implement exponential backoff: wait, retry, and double the wait on next limit
  3. Batch requests efficiently — use caching; don't re-render the same image twice
  4. Upgrade to PAYG for 10x higher limits (600/min instead of 60/min)

500 — Internal Server Error (internal)

Unexpected server error.

Billed: No (failures are never billed)

Causes:

  • Satori or resvg wasm module crashed (very rare)
  • Database connection failed (very rare)
  • Cloudflare infrastructure issue (transient)

Example response:

{
  "error": {
    "code": "internal",
    "message": "Internal server error"
  }
}

Stack traces are never sent to clients.

How to fix:

  1. Retry with exponential backoff
  2. Check status.renderog.app for incidents
  3. Email support@renderog.app if the error persists

501 — Not Implemented (not_available)

Feature not yet available.

Billed: No

Causes:

  • Calling /screenshot before Phase 5 (Browser Rendering) is enabled
  • Requesting an unsupported format like WebP on an older version

Example response:

{
  "error": {
    "code": "not_available",
    "message": "Screenshot API is coming soon (Phase 5)"
  }
}

How to fix: Wait for the feature to be enabled, or use /og (currently live).


Summary Table

HTTP Code Billed? Retry? Cause
400 invalid_params No No Bad input
401 invalid_key No No Missing/revoked API key
402 quota_exceeded No No Upgrade or wait
403 invalid_signature No No Wrong signature or expired
403 expired No No Signature timestamp past
422 render_failed No Yes (maybe) CSS/HTML error in template
429 rate_limited No Yes (with backoff) Too many requests
500 internal No Yes (backoff) Server error
501 not_available No No Feature not live yet

Best Practices

  1. Always check the HTTP status — your HTTP client library will bubble up 4xx/5xx; don't silently ignore them
  2. Parse error.code for programmatic handling (e.g., show users a message, retry, or escalate)
  3. Use Retry-After when retrying 429 errors
  4. Never retry 400, 401, 402, 403 — these need manual intervention
  5. Log errors with context — save the request URL, parameters (excluding secrets), and error response for debugging

See Quickstart and OG Reference for working examples.