Upload images (key or sign-in)
/v1/file/upload/sessionThe same upload as POST /v1/file/upload — identical body, limits,
per-file checks and response — but it also accepts a signed-in
session, so code running in the Stylor dashboard can use it. It
accepts, in this order:
- a private API key as a Bearer token, or
- a signed-in Stylor session cookie when no valid Bearer token is present.
A public key is refused with 403: public keys ship in storefront
code, and uploads are not a storefront operation. A private key that is
malformed, unknown or revoked is not an error by itself: the request
falls through to the session check, and is refused only if there is no
session either.
With a private key the files belong to the key's organization. With a session, they belong to one organization the signed-in account is an active member of (the first one found — it cannot be chosen); an account with no organization is refused.
Checks run in this order: authentication, the per-IP rate limit, the
session's organization, then the Content-Type and body.
This endpoint is not metered against your plan: it draws no quota,
returns no X-RateLimit-* headers, and the shared key/quota errors
listed for other endpoints do not apply — only the errors below can
occur. Instead it is limited to 30 requests per minute per client IP
(bucket api.v1.file.upload.session), counted after authentication
succeeds. The 429 carries no Retry-After header; read retryAfter
from the body.
Request body
multipart/form-datarequiredOne or more image files. Any file part is accepted regardless of field name. Only the first 10 file parts are read.
Response
Files that were stored.
Unique id of the stored file. It is the UUID portion of key.
Object key of the form uploads/<fileId>.<ext>. Pass it to GET /v1/image/{key} or to any field that accepts an image key.
The original filename after sanitising (path stripped, unsafe characters removed, at most 255 characters). unnamed when none was sent.
The declared and verified MIME type.
image/jpegimage/pngimage/webpimage/avifSize in bytes.
Hex SHA-256 of the stored bytes, computed as the file streamed in. Compare it with a local hash to confirm the upload arrived intact.
Pixel width, or null if it could not be read.
Pixel height, or null if it could not be read.
Files that were rejected.
The sanitised original filename.
Why the file was rejected. One of the per-file messages listed in the endpoint description.
Counts for the batch.
Files processed (succeeded + failed). At most 10.
Number of entries in files.
Number of entries in failures.
failures.Files that were stored.
Unique id of the stored file. It is the UUID portion of key.
Object key of the form uploads/<fileId>.<ext>. Pass it to GET /v1/image/{key} or to any field that accepts an image key.
The original filename after sanitising (path stripped, unsafe characters removed, at most 255 characters). unnamed when none was sent.
The declared and verified MIME type.
image/jpegimage/pngimage/webpimage/avifSize in bytes.
Hex SHA-256 of the stored bytes, computed as the file streamed in. Compare it with a local hash to confirm the upload arrived intact.
Pixel width, or null if it could not be read.
Pixel height, or null if it could not be read.
Files that were rejected.
The sanitised original filename.
Why the file was rejected. One of the per-file messages listed in the endpoint description.
Counts for the batch.
Files processed (succeeded + failed). At most 10.
Number of entries in files.
Number of entries in failures.
Errors
13Every error is JSON with a single error string unless noted. Match on the status code; the message is for humans.
Failed to parse uploadThe multipart body could not be parsed — it is malformed or truncated, or the multipart/form-data header has no boundary parameter.
No files were stored.Every file's stream broke off mid-upload (File stream error). The body also carries files, failures and summary.
Authentication required. Provide a Bearer token or sign in.No valid API key was sent and there is no signed-in session. A malformed or unknown key falls through to the session check before this is returned.
Public keys cannot be used here. Use a private key or sign in.The Bearer token is a public key (sgpt-pk-…). Checked before the session.
No organization is associated with this account.Session authentication succeeded but the account is not a member of any organization.
Only superusers can choose an upload folder.The request carried a folder query parameter. It is reserved for Stylor staff; leave it out and files are stored under uploads/.
No files were stored.Every file exceeded 10 MB. The body also carries files (empty), failures and summary; read failures[].error.
{
"error": "No files were stored.",
"files": [],
"failures": [
{
"filename": "huge.png",
"error": "File exceeds maximum size of 10 MB"
}
],
"summary": {
"total": 1,
"succeeded": 0,
"failed": 1
}
}Content-Type must be multipart/form-dataThe request's Content-Type header is not multipart/form-data.
No files were stored.Every file declared an unaccepted type, or its bytes did not match the declared type. The body also carries files (empty), failures and summary; read failures[].error.
{
"error": "No files were stored.",
"files": [],
"failures": [
{
"filename": "notes.txt",
"error": "Unsupported file type: text/plain. Allowed: image/jpeg, image/png, image/webp, image/avif"
}
],
"summary": {
"total": 1,
"succeeded": 0,
"failed": 1
}
}No files providedThe request has no body, or the body parsed but contained no file parts.
No files were stored.Every file was rejected, for differing client-side reasons or because a file was under 1 KB. The body also carries files (empty), failures and summary; read failures[].error.
Rate limit exceeded.More than 30 requests in the current minute from this IP. The body also carries bucket, resource, used, limit, retryAfter (seconds) and windowSeconds.
{
"error": "Rate limit exceeded.",
"bucket": "ip",
"resource": "api.v1.file.upload.session",
"used": 31,
"limit": 30,
"retryAfter": 42,
"windowSeconds": 60
}Internal server error.An unexpected failure on the server, including every file failing to store. The message is always this string; when files were involved the body also carries files, failures and summary.
curl -X POST "https://stylor.ai/api/v1/file/upload/session" \
-H "Authorization: Bearer $STYLOR_API_KEY" \
-F "file=@./catalogue.csv"{
"files": [
{
"fileId": "6d1e5c0a-2b3f-4a8e-9c7d-0f1e2a3b4c5d",
"key": "uploads/6d1e5c0a-2b3f-4a8e-9c7d-0f1e2a3b4c5d.jpg",
"filename": "background.jpg",
"contentType": "image/jpeg",
"size": 482113,
"sha256": "9f2b6c1d4e8a7b3c0d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c",
"width": 1600,
"height": 1067
}
],
"failures": [],
"summary": {
"total": 1,
"succeeded": 1,
"failed": 0
}
}