Uploads API

Send files to Valise and link them to records.

Uploads are files attached to Valise records. An upload could be an image on an artwork or presentation, or a document on a sale, consignment, or expense.

Working with uploads happens across a few steps: creating an upload record, sending the file bytes, and then linking the upload to the relevant record. All requests require an API key, as described in the API overview.

Currently only artwork images are supported, though we intend to add support for all upload types over time.

How uploads work

  1. Create the upload with POST /v0/uploads. The response is an upload with status: "pending" and an uploadFile saying where to send the file.
  2. Send the file with a PUT request to uploadFile.url, using the uploadFile.headers and the raw file as the request body. The response is the upload, now ready.
  3. Link the upload to its record. For artwork images that’s the add an image endpoint, which takes the upload’s id.

Link an upload within an hour of creating it. After that, uploads with no links are removed along with their files.

Here’s a JS example, where file is a File or Blob and artworkId is the ID of the artwork to attach it to:

const headers = {
  Authorization: "Bearer <API_KEY>",
  "Content-Type": "application/json",
}
 
// 1. Create the upload
const created = await fetch("https://api.valise.works/v0/uploads", {
  method: "POST",
  headers,
  body: JSON.stringify({
    kind: "artworkImage",
    filename: file.name,
    contentType: file.type,
    size: file.size,
  }),
})
const { data: upload } = await created.json()
 
// 2. Send the file
await fetch(upload.uploadFile.url, {
  method: upload.uploadFile.method,
  headers: upload.uploadFile.headers,
  body: file,
})
 
// 3. Link it to the artwork
await fetch(`https://api.valise.works/v0/artworks/${artworkId}/images`, {
  method: "POST",
  headers,
  body: JSON.stringify({ uploadId: upload.id }),
})

Create an upload

Create an upload for a file. Requires a key with read and write permission.

POST /v0/uploads
FieldTypeDescription
kindstringWhat the file is for. artworkImage is the only kind today, and accepts images only. Required.
filenamestringThe file’s name, as it should appear in Valise. Required.
contentTypestringThe file’s MIME type, if you know it. It’s echoed in the uploadFile headers. Valise detects the type from the file itself either way.
sizenumberThe file’s size in bytes, if you know it. A file over your plan’s limits is refused here, before you send it.
curl https://api.valise.works/v0/uploads
  --method POST
  --header "Authorization: Bearer <API_KEY>"
  --header "Content-Type: application/json"
  --data '{"kind": "artworkImage", "filename": "my-artwork.jpg", "contentType": "image/jpeg", "size": 4183212}'

On success, the API returns a 201 status code with the upload and where to send its file:

{
  "data": {
    "id": "d2qv94c5i88kavrvigcg",
    "status": "pending",
    "filename": "my-artwork.jpg",
    "createdAt": "2024-09-06T11:17:15.150Z",
    "uploadFile": {
      "url": "https://api.valise.works/v0/upload?id=d2qv94c5i88kavrvigcg&kind=artworkImage&expires=1725622035&signature=...",
      "method": "PUT",
      "headers": { "Content-Type": "image/jpeg" },
      "expiresAt": "2024-09-06T11:27:15Z"
    }
  }
}

The upload object

FieldTypeDescription
idstringThe upload’s ID. Pass it when linking the upload to a record. Once linked, it’s also the image’s id on the artwork.
statusstringpending until the file arrives, then ready.
filenamestringThe file’s name.
createdAtstringWhen the upload was created.
uploadFileobjectWhere to send the file. Only present on the response that created the upload.

An image’s dimensions and URL are reported on the artwork once the upload is linked, not on the upload.

The uploadFile object

FieldTypeDescription
urlstringThe URL to send the file to.
methodstringThe HTTP method to use. Always PUT.
headersobjectHeaders to send with the request, verbatim. Empty if you didn’t give a contentType.
expiresAtstringWhen the URL stops accepting the file, ten minutes after the upload was created.

The URL is signed over the upload’s ID, its kind, and its expiry, and stops working if any of them change. Use it exactly as given. If the URL expires before you’ve sent the file, create the upload again to get a new one.

Send the file

PUT <uploadFile.url>

The request body is the raw file, not a form. A Content-Length header is required: Valise uses it to refuse a file over your plan’s limits before reading the body. Most HTTP clients set it for you when the body is a file.

curl "https://api.valise.works/v0/upload?id=d2qv94c5i88kavrvigcg&kind=artworkImage&expires=1725622035&signature=..."
  --upload-file my-artwork.jpg
  --header "Content-Type: image/jpeg"

On success, the API returns a 200 status code with the upload, now ready:

{
  "data": {
    "id": "d2qv94c5i88kavrvigcg",
    "status": "ready",
    "filename": "my-artwork.jpg",
    "createdAt": "2024-09-06T11:17:15.150Z"
  }
}

Each URL accepts one file, and sending another returns a 409. If a file is refused, the upload stays pending, so you can fix the file and send it to the same URL while it’s still valid.

Limits and errors

Files are checked against your plan’s limits twice: when you create the upload, if you include the file’s size, and again when the file arrives. A file larger than your plan allows, or one that would exceed your storage, returns a 413, and the message says which limit was hit.

Images are limited to a maximum of 150 megapixels. Larger images should be stored as documents.

Creating an upload uses the response codes shared by every endpoint. Sending the file has its own:

StatusMeaning
400The file is empty, isn’t a type the upload’s kind accepts, is over 150 megapixels, or can’t be read.
403The upload URL has expired or was changed.
404The upload was removed before the file arrived.
409The upload already has its file.
411The request has no Content-Length header.
413The file is larger than your plan allows, or would exceed your storage.

Have more questions?

We're here to help. If you didn't find what you were looking for, or have more questions feel free to reach out.

Contact Us