Getting Started
Learn how to interact with the oJo API.
What you get
- Synchronous by default. A render request returns the finished file in the same HTTP response —
{ id, imageUrl, createdAt }for an image,{ id, pdfUrl, pages, createdAt }for a PDF. No job id to poll, no webhook to receive, no second request. - Bulk in one call.
POST /v1/image/bulkrenders many images from many templates in a single request, at 1 credit per image. - 60 renders per minute. Image and PDF endpoints share 60 requests per 60 seconds; template endpoints allow 30. See Rate Limits.
- Your files do not expire. Rendered images and PDFs stay at their URL for as long as your account is active. Nothing removes them on a timer — a file is deleted when you delete it.
- 30 credits to start, no card. Signing up issues 30 trial credits, valid for 30 days. No payment method is required to get them.
- Renders in about a second. Measured against production on 2026-08-11: a saved template renders in ~0.6 s at the median, self-contained HTML in ~0.85 s, and HTML that pulls external fonts or images in ~1.2 s — p95 under 1.8 s in all three cases. A measurement, not an SLA.
Base URL
https://api.ojo.soThe API is described in OpenAPI 3.1 at https://api.ojo.so/openapi.json . It needs no API key.
Authentication
To authenticate with the API, you need to provide an API key in the Authorization header.
Authorization: Bearer <your API key>You can obtain your API key from the API settings .
Response Codes
The API uses standard HTTP response codes to indicate the success or failure of an API request.
200 OK- Everything worked as expected.202 Accepted- Sent withPrefer: respond-async: the render is queued and its status is at theLocationaddress. See Async renders.400 Bad Request- The body is not valid JSON, a bulk request asks for more images than the rate limit allows, or theIdempotency-Keyis not valid (codeisidempotency_key_invalid). See Safe retries.401 Unauthorized- No valid API key provided.402 Payment Required- Not enough credits for the request. The message links to the plans.403 Forbidden- The account is suspended, or the API key may not use this endpoint. A key created for a connected AI app cannot delete.404 Not Found- The requested resource doesn’t exist, or is not yours.405 Method Not Allowed- The path exists but does not take this method. TheAllowheader lists the methods it takes.409 Conflict- A request with the sameIdempotency-Keyis still rendering.codeisidempotency_key_in_progress, andretryAftergives the wait in seconds.410 Gone- The image transform endpoint was removed. Render images withPOST /v1/image/htmlorPOST /v1/image/template.413 Payload Too Large- The request body is larger than 1 MB.415 Unsupported Media Type- The body is not of the type the endpoint reads.logsnames the one it wants:text/htmlfor the HTML endpoints,application/jsonfor the JSON ones.422 Unprocessable Entity- The request cannot be served as sent, or the render failed.logssays why.codeisidempotency_key_reusedwhen theIdempotency-Keywas sent before with a different request.429 Too Many Requests- Over the rate limit, or another multi-page PDF from your account is still rendering.codenames which, andretryAftergives the wait in seconds. See Rate Limits.500 Internal Server Error- We had a problem with our server. Try again later.503 Service Unavailable- A service the request needs is unavailable.codenames it:queue_fullwhen 100 renders are already waiting,queue_unavailablewhen the render queue cannot be reached,render_unavailablewhen the render failed on our side,database_unavailablewhen oJo could not reach its database, andauth_unavailablewhen it could not check your API key.Retry-AfterandretryAftergive the wait in seconds, as in a 429.504 Gateway Timeout- The render was not done within 90 seconds.codeisrender_timeout, and nothing is charged.
Rate Limits
The oJo API implements endpoint-specific rate limiting to ensure fair usage and system stability. Rate limits are tracked per user and enforced across all API requests.
Current Rate Limits
| Endpoint Type | Rate Limit | Time Window | Block Duration | code in the 429 |
|---|---|---|---|---|
| Image and PDF Operations | 60 requests | 60 seconds | 120 seconds | rate_limited.render |
| Template Operations | 30 requests | 60 seconds | 120 seconds | rate_limited.template |
| PDF Preview (editor) | 30 requests | 1 hour | none | rate_limited.pdf_preview |
| Credits | 30 requests | 60 seconds | none | rate_limited.credits |
One multi-page PDF (pages) renders per account at a time. Another one sent meanwhile gets a 429
with code: "pdf_in_progress" until the first finishes.
When you make an API request, the response will include headers that provide information about your current rate limit status:
X-RateLimit-Policy: <endpoint-type>:<requests>/<seconds>s
X-RateLimit-Limit: <maximum requests allowed>
X-RateLimit-Remaining: <requests remaining>
X-RateLimit-Reset: <ISO timestamp when the limit resets>Rate Limit Exceeded
If you exceed the rate limit, you’ll receive a 429 Too Many Requests response with the following headers:
HTTP/1.1 429 Too Many Requests
X-RateLimit-Policy: <endpoint-type>:<requests>/<seconds>s
X-RateLimit-Limit: <maximum requests allowed>
X-RateLimit-Remaining: 0
X-RateLimit-Reset: <ISO timestamp when the limit resets>
Retry-After: <seconds until next request is allowed>The body names the cause, the limit and the wait. retryAfter equals Retry-After:
{
"error": true,
"code": "rate_limited.render",
"message": "Render rate limit reached: 60 image and PDF renders per minute",
"logs": ["Try again in 2 minutes."],
"retryAfter": 87
}Best Practices
To work effectively with these rate limits:
- Monitor the
X-RateLimit-Remainingheader to track your remaining requests - Use the
X-RateLimit-Resettimestamp to schedule requests appropriately - When receiving a 429 response, wait for the duration specified in the
Retry-Afterheader before retrying - Implement exponential backoff in your retry logic
- Consider implementing request queuing for high-volume operations
Example Response Headers
For image and PDF operations:
X-RateLimit-Policy: image:60/60s
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 2024-11-06T15:30:45.066Z
Retry-After: 87For template operations:
X-RateLimit-Policy: template:30/60s
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 29
X-RateLimit-Reset: 2024-11-06T15:30:45.066ZResource Limits
Your HTML runs in a browser, JavaScript included. Browser storage (localStorage,
sessionStorage, IndexedDB, the Cache API) and Web Workers are off, so a library that needs them
will not work.
Images, fonts, stylesheets and scripts load from public https:// addresses. Images can also be
data: URLs. Plain http:// and private or internal addresses are refused.
| Limit | Value |
|---|---|
| Each document, script, stylesheet or font | 1 MB |
| Each image | 5 MB |
| All resources of one page together | 10 MB |
| Requests per page | 100 |
Requests a page may attempt, refused, cached and data: ones included | 500, past it the render fails with a 422 |
URLs of those requests together, data: URLs included | 16 MB, past it the render fails with a 422 |
| Requests in flight at once, per page | 6 (more wait their turn) |
| Port | 443 only |
| Time to fetch one resource | 5 s |
| Redirects per resource | 5 |
| Page load | 5 s |
Wait for render-complete (charts) | 5 s |
| Rendering one image | 30 s |
| Rendering one PDF | 60 s |
| Rendered HTML of an image template | 5 MB |
A resource that breaks a limit or is refused fails the render. When render-complete does not
appear within 5 s, the page is captured as it is. A render past its time limit gets a 422 that names
the limit, and nothing is charged.
Caveats
oJo API is currently in public beta and subject to change. We will do our best to notify you of any changes.