Documentation
Rods & Cones API
We have a single endpoint that takes an image and upscales it to four times the resolution.
The endpoint is asynchronous: once your image is uploaded it returns a job id right away, then calls back to a webhook URL you provide in the request with a signed URL to the upscaled result. Note that the first request can take up to a minute to reach your webhook, while an idle GPU spins up.
Getting started
- 01Sign in, then open API keys from the top right of the dashboard.
- 02Give the key a label, usually the server that will use it, then press Create key.
- 03Copy both secrets out of the panel that appears: the key (
rc_…) and the webhook signing secret (whsec_…). These secrets will never be shown again, so store them somewhere safe. - 04Export the API key (the one starting with rc_) to an environment variable:
export RC_KEY="rc_xxxxxxxxxxxxxxxxxxxxxxxx"
You are all set to call the API now.
Quick start
Submit an image and a callback_url. The endpoint reserves the latents, accepts the job, and returns 202 right away with a job id.
curl -X POST https://www.rodsandcones.ai/api/v1/upscale \
-H "Authorization: Bearer $RC_KEY" \
-F image=@lioness.jpg \
-F steps=1 \
-F callback_url=https://hooks.example.com/rcYou will receive a response like so, signifying your upscale job is running:
HTTP 202
{
"id": "6f1c9d84-2b7e-4a10-9f33-0c5a8e2d4471",
"status": "processing",
"steps": 1,
"latents_used": 1,
"latents_remaining": 943,
"created_at": "2026-07-30T09:14:20.000Z"
}When the upscale finishes, we POST the result to your callback_url. That is the normal way to get the image back, see: Webhooks. If a delivery is missed, the GET endpoint can be polled: poll the job by its id.
POST /api/v1/upscale
The output is always four times the input dimensions, so a 480 × 300 source comes back at 1920 × 1200.
The API exposes some parameters that the model can use to produce results faster or with higher quality. They are described below:
| Parameter | Type | Default | Description |
|---|---|---|---|
| image | file | required | The image file to upscale. Supported formats: JPEG, PNG, and WebP. Max file size: 25 MB. Max resolution: 2048 × 2048. |
| callback_url | url | required | An https URL we POST the result to when the job finishes. Must be publicly reachable. |
| steps | integer | 1 | An integer from 1 to 8. Costs one latent per step, so steps=3 spends 3 latents. More steps mean higher quality but slower processing. |
The acceptance response and every error carry these headers:
| Header | Meaning |
|---|---|
| x-rc-request-id | Identifier for this call. Please try to include it when contacting support. |
| x-rc-latents-used | Latents reserved for this job. |
| x-rc-latents-remaining | Your balance after the reservation. |
id is the job id. The same job shows up in your dashboard history, and you use the id to poll status. The result arrives at your webhook.
Latents are reserved on acceptance and refunded automatically if the job later fails, so you are only charged for an upscale that is actually produced.
Webhooks
When a job settles we POST a JSON body to the callback_url you submitted. A successful delivery looks like this:
POST https://hooks.example.com/rc
X-RC-Event: job.succeeded
X-RC-Delivery: 4d1c7a90-0b2e-4c3a-8f21-9e5b0c6d7a88
X-RC-Timestamp: 1785489262
X-RC-Signature: sha256=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
{
"id": "6f1c9d84-2b7e-4a10-9f33-0c5a8e2d4471",
"status": "succeeded",
"output": {
"width": 1920,
"height": 1200,
"url": "https://<storage-host>/outputs/...?X-Amz-Signature=...",
"expires_at": "2026-08-06T09:14:22.000Z"
},
"steps": 1,
"latents_used": 1,
"duration_ms": 2840
}A failed job delivers X-RC-Event: job.failed and a body of { "id", "status": "failed", "error", "latents_remaining" }.
| Header | Meaning |
|---|---|
| X-RC-Event | job.succeeded or job.failed. |
| X-RC-Delivery | Unique id for this delivery attempt. |
| X-RC-Timestamp | Unix seconds when we signed the body. |
| X-RC-Signature | HMAC-SHA256 of the signed payload, as sha256=<hex>. |
Verifying callbacks: The signed payload is the timestamp, a dot, and the raw request body. Recompute the HMAC with your whsec_ secret and compare to verify the callback actually came from us:
import crypto from "node:crypto";
// req.rawBody is the exact bytes we sent — verify before JSON.parse.
function verify(rawBody, headers, secret) {
const ts = headers["x-rc-timestamp"];
const expected =
"sha256=" +
crypto.createHmac("sha256", secret)
.update(ts + "." + rawBody)
.digest("hex");
const got = headers["x-rc-signature"];
const ok =
got.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected));
// Reject anything older than a few minutes to stop replays.
const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
return ok && fresh;
}GET /api/v1/jobs/{id}
Poll a job by its id with the same bearer used to create the job.
curl https://www.rodsandcones.ai/api/v1/jobs/$JOB_ID \
-H "Authorization: Bearer $RC_KEY"Response looks like:
{
"id": "6f1c9d84-2b7e-4a10-9f33-0c5a8e2d4471",
"status": "succeeded",
"output": {
"width": 1920,
"height": 1200,
"url": "https://<storage-host>/outputs/...?X-Amz-Signature=...",
"expires_at": "2026-08-06T09:14:22.000Z"
},
"steps": 1,
"latents_used": 1,
"duration_ms": 2840,
"latents_remaining": 943
}Latents and limits
“Latent” is the name of the in-app currency we use; think of it as the unit of work. One latent buys one upscale at one step. In this sense, pricing is not dependent on image size.
More questions? Need help?
If you have any questions, feedback, or complaints, please don't hesitate to reach out to us at support@rodsandcones.ai.