---
title: Error Codes
description: Complete HTTP status codes and error codes reference. Billing status for each error.
slug: errors
---

# Error Codes & HTTP Status Reference

Every error response is JSON with this structure:

```json
{
  "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:**
```json
{
  "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](./og.md) 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:**
```json
{
  "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](https://renderog.app/dashboard).

---

## 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:**
```json
{
  "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](https://renderog.app/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:**
```json
{
  "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](./quickstart.md) 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:**
```json
{
  "error": {
    "code": "render_failed",
    "message": "Satori render failed: unsupported CSS property 'grid'"
  }
}
```

**How to fix:**
1. Use the [Playground](https://renderog.app/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](./og.md#satori--css-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:**
```json
{
  "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:**
```json
{
  "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](https://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:**
```json
{
  "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](./quickstart.md) and [OG Reference](./og.md) for working examples.
