Uploading an image
Upload your own image so it can be used as input.
Some endpoints take an image you supply rather than only a prompt. You upload it once, get back an id, and pass that id wherever an image input is accepted.
PierNode does not fetch images from URLs you provide. Uploading is the only way to bring your own image in, which keeps every input under your account and inside storage PierNode controls.
Uploads are not billed.
The flow
POST /api/v1/uploads— reserve an upload and get a URL to send bytes to.PUTthe file to that URL.POST /api/v1/uploads/{assetId}/complete— PierNode checks the file and marks it ready.
Only after step 3 is the upload usable.
1. Create the upload
curl -X POST https://app.piernode.com/api/v1/uploads \
-H "Authorization: Bearer $PIERNODE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"contentType": "image/png"}'Request
| Field | Type | Required | Notes |
|---|---|---|---|
contentType | string | yes | image/jpeg, image/png or image/webp. |
That is the whole request. There is no way to choose where the file is stored, and any other field you send is ignored.
Response
{
"id": "c8f2a1d4e5b64c9a8f3d2e1b0a9c8d7e",
"uploadUrl": "https://...",
"method": "PUT",
"expiresAt": "2026-01-01T12:15:00.000Z"
}Keep the id. The uploadUrl is for the next step only and is not needed again.
2. Send the bytes
PUT the file to uploadUrl with the same Content-Type you declared. The
URL is signed for that content type, so a different one is rejected.
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: image/png" \
--data-binary @photo.pngA successful PUT means the bytes arrived. It does not mean the file was
accepted — that is decided in the next step.
3. Complete the upload
curl -X POST https://app.piernode.com/api/v1/uploads/\{assetId\}/complete \
-H "Authorization: Bearer $PIERNODE_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'PierNode reads the file's header and confirms it really is the image type you declared, then records its dimensions.
{
"id": "c8f2a1d4e5b64c9a8f3d2e1b0a9c8d7e",
"status": "READY",
"contentType": "image/png",
"sizeBytes": 284913,
"width": 1024,
"height": 768,
"createdAt": "2026-01-01T12:00:00.000Z",
"expiresAt": "2026-01-01T12:15:00.000Z",
"verifiedAt": "2026-01-01T12:00:42.000Z",
"error": null
}This endpoint is safe to call more than once: once an upload has settled, the same answer comes back without the file being read again.
Status values
| Status | Meaning |
|---|---|
PENDING | Reserved; the file has not been verified yet. |
READY | Verified and usable as input. |
INVALID | The file was rejected. See error.code. |
EXPIRED | The upload window closed before the upload was completed. |
An upload that reaches INVALID or EXPIRED stays that way. Uploading a
different file afterwards does not change the verdict — create a new upload.
Rejection reasons
error.code | Meaning |
|---|---|
UNSUPPORTED_FORMAT | The bytes are not a JPEG, PNG or WebP. |
CONTENT_TYPE_MISMATCH | The file is an image, but not the type you declared. |
FILE_TOO_LARGE | Larger than 20 MB. |
FILE_TOO_SMALL | Too small to be a real image. |
DIMENSIONS_OUT_OF_RANGE | Outside the accepted size range. |
UPLOAD_EXPIRED | The upload window closed first. |
If you call complete before the bytes have arrived, you get a 400 and the
upload stays PENDING, so you can retry.
Limits
| Limit | Value |
|---|---|
| Formats | JPEG, PNG, WebP |
| Maximum file size | 20 MB |
| Dimensions | 16–8192 px per side, up to 40 megapixels |
| Upload window | 15 minutes |
The declared content type is only a hint. What the file actually is gets decided
by reading it, so renaming a .zip to .png will not get it through.
4. Check an upload
curl https://app.piernode.com/api/v1/uploads/\{assetId\} \
-H "Authorization: Bearer $PIERNODE_API_KEY"Returns the same shape as complete. Uploads belonging to another account
return 404, exactly as an id that does not exist does.
How long URLs last
Three different links are involved, and they expire on different clocks:
| Link | Lifetime | What it is for |
|---|---|---|
| Upload URL | 15 minutes | The one-off PUT in step 2. Covers one file and nothing else. |
| Generation input | 30 minutes | Minted internally when a generation reads your image. Never returned to you. |
| Generation output | 60 minutes | Result links from GET /api/v1/tasks/\{taskId\}. Re-poll the task for a fresh one. |
Refer to an upload by its id. That id does not expire while the upload exists,
so it is the thing worth storing.