Upload images (key or sign-in)

post/v1/file/upload/session

The 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:

  1. a private API key as a Bearer token, or
  2. 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.

Private keyNo demo

Request body

multipart/form-datarequired
filearray<string (binary)>required

One or more image files. Any file part is accepted regardless of field name. Only the first 10 file parts are read.

maxItems: 10

Response

200Every file was stored.
array<UploadedFile>required

Files that were stored.

fileIdstring (uuid)required

Unique id of the stored file. It is the UUID portion of key.

e.g. "6d1e5c0a-2b3f-4a8e-9c7d-0f1e2a3b4c5d"
keystringrequired

Object key of the form uploads/<fileId>.<ext>. Pass it to GET /v1/image/{key} or to any field that accepts an image key.

e.g. "uploads/6d1e5c0a-2b3f-4a8e-9c7d-0f1e2a3b4c5d.jpg"
filenamestringrequired

The original filename after sanitising (path stripped, unsafe characters removed, at most 255 characters). unnamed when none was sent.

maxLength: 255e.g. "background.jpg"
contentTypestringrequired

The declared and verified MIME type.

One ofimage/jpegimage/pngimage/webpimage/avif
e.g. "image/jpeg"
sizeintegerrequired

Size in bytes.

min: 1024max: 10485760e.g. 482113
sha256stringrequired

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.

pattern: ^[0-9a-f]{64}$e.g. "9f2b6c1d4e8a7b3c0d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c"
widthintegernullablerequired

Pixel width, or null if it could not be read.

e.g. 1600
heightintegernullablerequired

Pixel height, or null if it could not be read.

e.g. 1067
array<UploadFailure>required

Files that were rejected.

filenamestringrequired

The sanitised original filename.

e.g. "logo.svg"
errorstringrequired

Why the file was rejected. One of the per-file messages listed in the endpoint description.

e.g. "Unsupported file type: image/svg+xml. Allowed: image/jpeg, image/png, image/webp, image/avif"
objectrequired

Counts for the batch.

totalintegerrequired

Files processed (succeeded + failed). At most 10.

e.g. 2
succeededintegerrequired

Number of entries in files.

e.g. 1
failedintegerrequired

Number of entries in failures.

e.g. 1
207Some files were stored and some were rejected. Inspect failures.
array<UploadedFile>required

Files that were stored.

fileIdstring (uuid)required

Unique id of the stored file. It is the UUID portion of key.

e.g. "6d1e5c0a-2b3f-4a8e-9c7d-0f1e2a3b4c5d"
keystringrequired

Object key of the form uploads/<fileId>.<ext>. Pass it to GET /v1/image/{key} or to any field that accepts an image key.

e.g. "uploads/6d1e5c0a-2b3f-4a8e-9c7d-0f1e2a3b4c5d.jpg"
filenamestringrequired

The original filename after sanitising (path stripped, unsafe characters removed, at most 255 characters). unnamed when none was sent.

maxLength: 255e.g. "background.jpg"
contentTypestringrequired

The declared and verified MIME type.

One ofimage/jpegimage/pngimage/webpimage/avif
e.g. "image/jpeg"
sizeintegerrequired

Size in bytes.

min: 1024max: 10485760e.g. 482113
sha256stringrequired

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.

pattern: ^[0-9a-f]{64}$e.g. "9f2b6c1d4e8a7b3c0d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c"
widthintegernullablerequired

Pixel width, or null if it could not be read.

e.g. 1600
heightintegernullablerequired

Pixel height, or null if it could not be read.

e.g. 1067
array<UploadFailure>required

Files that were rejected.

filenamestringrequired

The sanitised original filename.

e.g. "logo.svg"
errorstringrequired

Why the file was rejected. One of the per-file messages listed in the endpoint description.

e.g. "Unsupported file type: image/svg+xml. Allowed: image/jpeg, image/png, image/webp, image/avif"
objectrequired

Counts for the batch.

totalintegerrequired

Files processed (succeeded + failed). At most 10.

e.g. 2
succeededintegerrequired

Number of entries in files.

e.g. 1
failedintegerrequired

Number of entries in failures.

e.g. 1

Errors

13

Every error is JSON with a single error string unless noted. Match on the status code; the message is for humans.

400
Failed to parse upload

The multipart body could not be parsed — it is malformed or truncated, or the multipart/form-data header has no boundary parameter.

400
No files were stored.

Every file's stream broke off mid-upload (File stream error). The body also carries files, failures and summary.

401
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.

403
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.

403
No organization is associated with this account.

Session authentication succeeded but the account is not a member of any organization.

403
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/.

413
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
  }
}
415
Content-Type must be multipart/form-data

The request's Content-Type header is not multipart/form-data.

415
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
  }
}
422
No files provided

The request has no body, or the body parsed but contained no file parts.

422
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.

429
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
}
500
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
  }
}