Uploading Files

Understand the different methods for uploading files to Vulgate, including single and multipart uploads.

Upload modes

Ingesting files is a two-step process. First, you upload the files, and then start the ingestion process.

Vulgate supports two upload modes: single and multipart.

Single upload

To upload a file at once, follow these two steps:

  1. Obtain a signed URL for the file upload.
  2. Make a PUT request to the signed URL with the file content.

Obtaining a signed URL

To obtain a signed URL for the file upload, use the following endpoint:

GET /api/uploads

The following parameters are supported as URL search params:

ParameterTypeDefaultDescription
namestringRequiredThe name of the file
typestringRequiredThe content type of the file

For example:

GET /api/uploads?name=example.pdf&type=application/pdf

The response schema is as follows:

{
    "url": "https://your-signed-url.com",
    "method": "PUT",
    "headers": {
      "content-type": "application/pdf",
      "x-amz-meta-name": "example.pdf"
    }
}

Making a PUT request

To upload the file, make a PUT request to the signed URL with the file content. The request should include the headers specified in the response object.

For example:

PUT your-signed-url.com HTTP/1.1
Content-Type: application/pdf
X-Amz-Meta-Name: example.pdf
 
<file content>

Multipart upload

For large files, upload the file in parts using an S3-style multipart flow. The overall sequence is:

  1. Initiate the multipart upload to obtain an uploadId and an object key.
  2. For each part, request a signed URL, then PUT the chunk to that URL and keep the ETag returned in the response headers.
  3. Complete the upload by sending the ordered list of part numbers and their ETags.

Each part must be at least 5 MB, except the last part. Choose a part size, split the file into sequential parts numbered from 1, and upload them with the steps below. Use List parts to resume an interrupted upload and Abort to cancel one.

Initiating a multipart upload

POST /api/uploads

Send the file metadata in the request body:

{
  "file": {
    "name": "example.pdf",
    "type": "application/pdf",
    "size": 73400320,
    "meta": { "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }
  }
}
FieldTypeDefaultDescription
file.namestringRequiredThe name of the file
file.typestringRequiredThe content type of the file
file.sizenumberThe file size in bytes
file.meta.idstring (UUID)Optional client-supplied UUID. When valid it becomes the file’s id; otherwise one is generated.

The response returns the uploadId and key used by every subsequent request, plus the id you later pass to POST /api/jobs:

{
  "uploadId": "2~aBcD1234efGh5678",
  "key": "user_files/file_f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
}

Uploading a part

For each part, first request a signed URL:

POST /api/uploads/{uploadId}/parts/{partNumber}
ParameterTypeDefaultDescription
uploadIdstringRequiredThe uploadId from the initiate step (path segment)
partNumbernumberRequiredThe 1-based index of this part (path segment)
{ "key": "user_files/file_f47ac10b-58cc-4372-a567-0e02b2c3d479" }

The response contains a presigned PUT URL, valid for one hour:

{ "url": "https://your-signed-url.com" }

Then PUT the chunk directly to that URL and record the ETag from the response headers — you must supply it when completing the upload. Repeat for every part.

Completing the upload

POST /api/uploads/{uploadId}/complete

Send the object key and the parts in ascending PartNumber order. Use the exact ETag value returned by each part’s PUT response (S3 returns it as a quoted string):

{
  "key": "user_files/file_f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "parts": [
    { "PartNumber": 1, "ETag": "\"d8e8fca2dc0f896fd7cb4cb0031ba249\"" },
    { "PartNumber": 2, "ETag": "\"a420c5f7b1c0e9d3f8a1b2c3d4e5f607\"" }
  ]
}

The response describes the assembled object:

{
  "Location": "https://.../user_files/file_f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "Bucket": "vulgate",
  "Key": "user_files/file_f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "ETag": "\"f8c2e1d0b9a8765432100fedcba98765-2\""
}

Listing uploaded parts

POST /api/uploads/{uploadId}/parts

To resume an interrupted upload, list the parts already stored, then upload only the missing ones:

{ "key": "user_files/file_f47ac10b-58cc-4372-a567-0e02b2c3d479" }
[
  {
    "PartNumber": 1,
    "ETag": "\"d8e8fca2dc0f896fd7cb4cb0031ba249\"",
    "Size": 5242880,
    "LastModified": "2025-01-01T00:00:00.000Z"
  }
]

Aborting a multipart upload

DELETE /api/uploads/{uploadId}

Cancel a multipart upload and discard any uploaded parts:

{ "key": "user_files/file_f47ac10b-58cc-4372-a567-0e02b2c3d479" }

Full example

Step 1 — Initiate the multipart upload:

curl -X POST "https://vulgate.ai/api/uploads" \
  -H "Authorization: Bearer $VULGATE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "file": { "name": "large.pdf", "type": "application/pdf", "size": 73400320 }
  }'

Step 2 — For each part, get a signed URL and upload the chunk:

# Request a signed URL for part 1
curl -X POST "https://vulgate.ai/api/uploads/<upload-id>/parts/1" \
  -H "Authorization: Bearer $VULGATE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "key": "<object-key>" }'
 
# Upload the chunk to the signed URL; -D - prints the ETag response header
curl -X PUT "<signed-url>" \
  --data-binary @part-1.bin \
  -D -

Step 3 — Complete the upload with the collected ETags:

curl -X POST "https://vulgate.ai/api/uploads/<upload-id>/complete" \
  -H "Authorization: Bearer $VULGATE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "<object-key>",
    "parts": [
      { "PartNumber": 1, "ETag": "<etag-from-part-1>" },
      { "PartNumber": 2, "ETag": "<etag-from-part-2>" }
    ]
  }'

Once the upload is complete, pass the id from step 1 to POST /api/jobs to create a document and start ingestion.