API overview
Your server sends authenticated REST requests to Upscayl. Creating an upscale is asynchronous: the create endpoint returns a task immediately, and the result endpoint reports its latest state.
| Method | Endpoint | Purpose |
|---|---|---|
POST | /api/v1/upscales | Create one upscale task. |
GET | /api/v1/upscales/{id} | Retrieve status, result, and billing state. |
Polling is supported
Use the returned urls.get URL until the task reaches a terminal state.
Customer webhooks are not available yet
Do not send a callback URL. A separate signed webhook contract will be published before this option is enabled.
Quick start
Create a key, submit one image, then start polling after 3 seconds and gradually increase the interval up to 10 seconds.
- 1Create an API key
- 2Submit an image URL
- 3Download the result
Authentication
API access is activated after your first successful credit purchase. Add your secret key to the Authorization header and keep it on your server—never put it in browser or mobile application code.
/api/v1/upscalesCreate an upscale
The image must be available through a public HTTPS URL. A 202 Accepted response means the task was accepted, not that processing has finished.
curl --request POST \
--url https://www.upscayl.app/api/v1/upscales \
--header "Authorization: Bearer $UPSCAYL_API_KEY" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: order-1842-image-01" \
--data '{
"image_url": "https://example.com/photo.jpg",
"scale": 2,
"face_enhance": false,
"output_format": "png"
}'Required headers
| Header | Required | Description |
|---|---|---|
| Authorization | Yes | Bearer API key. |
| Content-Type | Yes | application/json |
| Idempotency-Key | Yes | 8–128 characters. Reuse it when retrying the same logical image request. |
JSON body
| Field | Type | Description |
|---|---|---|
| image_url | string | Required public HTTPS URL ending in .jpg, .jpeg, .png, or .webp. |
| scale | integer | 2 or 4. Defaults to 2. |
| face_enhance | boolean | Improve faces. Defaults to false. |
| output_format | string | png. Other formats are coming later. |
{
"id": "upscale-7d2f...",
"object": "upscale",
"status": "processing",
"urls": {
"get": "https://www.upscayl.app/api/v1/upscales/upscale-7d2f..."
},
"usage": {
"credits_reserved": 0.7,
"credits_charged": 0
}
}Save both id and urls.get. They are the source of truth if the client disconnects after submission.
/api/v1/upscales/{id}Check task status
Start after 3 seconds, use gradual backoff, and stop when status becomes succeeded or failed. GET requests may be safely retried after temporary network or server errors.
curl \
--url https://www.upscayl.app/api/v1/upscales/UPSCALING_TASK_ID \
--header "Authorization: Bearer $UPSCAYL_API_KEY"{
"id": "upscale-7d2f...",
"object": "upscale",
"status": "processing",
"urls": {
"get": "https://www.upscayl.app/api/v1/upscales/upscale-7d2f..."
},
"usage": {
"credits_reserved": 0.7,
"credits_charged": 0
}
}{
"id": "upscale-7d2f...",
"object": "upscale",
"status": "succeeded",
"urls": {
"get": "https://www.upscayl.app/api/v1/upscales/upscale-7d2f..."
},
"result": {
"url": "https://cdn.upscayl.app/hd-photos/result.png",
"format": "png"
},
"usage": {
"credits_charged": 0.7
}
}{
"id": "upscale-7d2f...",
"object": "upscale",
"status": "failed",
"error": {
"code": "image_processing_failed",
"message": "The image could not be processed. No credits were charged."
},
"usage": {
"credits_charged": 0,
"refund_status": "refunded"
}
}import os
import time
import requests
api_key = os.environ["UPSCAYL_API_KEY"]
headers = {
"Authorization": f"Bearer {api_key}",
"Idempotency-Key": "order-1842-image-01",
}
response = requests.post(
"https://www.upscayl.app/api/v1/upscales",
headers=headers,
json={"image_url": "https://example.com/photo.jpg", "scale": 2},
timeout=(10, 60),
)
response.raise_for_status()
task = response.json()
deadline = time.monotonic() + 300
interval = 3
while time.monotonic() < deadline:
response = requests.get(
task["urls"]["get"],
headers={"Authorization": headers["Authorization"]},
timeout=(10, 30),
)
response.raise_for_status()
result = response.json()
if result["status"] == "succeeded":
print(result["result"]["url"])
break
if result["status"] == "failed":
raise RuntimeError(result["error"]["message"])
time.sleep(interval)
interval = min(10, interval + 1)
else:
raise TimeoutError(f"Task {task['id']} did not finish within 5 minutes")Customer webhooks
The public API currently uses polling for result delivery. Upscayl does not send task events to customer callback URLs yet. Continue using urls.get; do not expose a webhook receiver solely for this integration.
When customer webhooks become available, they will be documented as a separate outbound contract covering event payloads, signature verification, duplicate delivery, acknowledgement time, retries, and polling fallback.
Retries and idempotency
Use a unique Idempotency-Key for each logical image request.
If POST times out or returns a temporary server error, retry with the same key and identical parameters.
The same key and parameters return the original task without another charge.
The same key with different parameters returns 409 idempotency_conflict.
To run a genuinely new task after a terminal failure, use a new key.
Billing
0.7 credit is reserved when a task is accepted.
A successful result charges 0.7 credit.
A confirmed failed task returns the reserved 0.7 credit.
A failed response can briefly report refund_status as pending while the refund is finalized.
Repeating the same Idempotency-Key returns the original task without another charge.
Limits
Errors
Errors use normal HTTP status codes and include a stable code plus a request ID for support.
{
"error": {
"type": "invalid_request_error",
"code": "invalid_scale",
"message": "scale must be 2 or 4.",
"request_id": "req_01abc..."
}
}| 400 | Invalid parameters, JSON, idempotency key, or image URL |
| 401 | Missing, invalid, or revoked API key |
| 402 | Not enough credits |
| 403 | Paid API access is required or the API key lacks permission |
| 404 | Task not found or not owned by this account |
| 409 | Idempotency key was reused with different parameters |
| 429 | Rate, concurrency, or daily limit reached |
| 503 | Service temporarily unavailable |
| 504 | An upstream request timed out |
Rate-limit response headers
X-RateLimit-Limit is the create-request limit, X-RateLimit-Remaining is the remaining count in the current window, and X-RateLimit-Reset is the Unix reset time. Honor Retry-After on 429 responses.
Result retention
Result URLs are intended for delivery, not as your permanent asset archive. Download and store every successful result you need to keep. Upscayl does not currently promise a minimum public URL retention period in the API contract.