Skip to main content

Uploading attachments via API

Written by Hugh Evans
Updated yesterday

Attachments such as photos, floor plans, or documents can be added to records created via the API. Files are uploaded independently first, then referenced when creating the record.

This two-step approach means file uploads are decoupled from record creation. You can upload multiple files in parallel before submitting the request that references them.

How it works

  1. Request a presigned upload URL from the Visibuild API for each file.

  2. Upload the file directly to S3 using the presigned URL and headers from the response.

  3. Reference the file by its key when creating a record (e.g. a ticket).

The presigned URL is a temporary, authenticated link that lets you upload directly to cloud storage without routing the file through the Visibuild API.

Step 1: Request a presigned URL

Call POST /api/core/v1/attachments with details about the file you want to upload.

POST https://app.visibuild.com.au/api/core/v1/attachments
Authorization: Bearer {access_token}
Content-Type: application/json

{
"filename": "defect-photo.jpg",
"contentType": "image/jpeg",
"fileSize": 204800,
"checksum": "base64-encoded-md5"
}

Request fields

Field

Type

Required

Description

filename

string

Yes

Original filename, e.g. "defect-photo.jpg".

contentType

string

Yes

MIME type, e.g. "image/jpeg", "application/pdf".

fileSize

integer

Yes

File size in bytes.

checksum

string

Yes

Base64-encoded MD5 checksum of the file contents.

Response

{
"data": {
"upload": {
"key": "attachment/abc123/defect-photo.jpg",
"filename": "defect-photo.jpg",
"url": "https://s3.amazonaws.com/bucket/...",
"headers": {
"Content-Type": "image/jpeg",
"Content-MD5": "base64-encoded-md5"
}
}
}
}

Save the key value. You will need it in Step 3.

Step 2: Upload the file to S3

Use the url and headers from the response to upload the file with an HTTP PUT request. This goes directly to S3, not through the Visibuild API.

PUT {upload.url}
Content-Type: image/jpeg
Content-MD5: {checksum}

[binary file data]

Ensure you include all headers returned in the headers object. The upload will fail if the content type or checksum does not match what was specified in Step 1.

Important: S3 will reject the upload if the fileSize or checksum you provided in Step 1 does not match the actual file. Make sure you calculate both from the real file contents, not from metadata or estimates.

Computing the checksum

The checksum field must be the Base64-encoded MD5 hash of the file's binary contents.

Node.js

const fs = require('fs');
const crypto = require('crypto');

const fileBuffer = fs.readFileSync('defect-photo.jpg');
const checksum = crypto.createHash('md5').update(fileBuffer).digest('base64');
const fileSize = fileBuffer.length;

Python

import hashlib, base64, os

with open('defect-photo.jpg', 'rb') as f:
contents = f.read()

checksum = base64.b64encode(hashlib.md5(contents).digest()).decode()
file_size = len(contents)

Ruby

require 'digest'
require 'base64'

contents = File.binread('defect-photo.jpg')
checksum = Base64.strict_encode64(Digest::MD5.digest(contents))
file_size = contents.bytesize

Uploading with the presigned URL

Node.js

const response = await fetch(upload.url, {
method: 'PUT',
headers: upload.headers,
body: fileBuffer,
});
// response.status should be 200

Python

import requests

with open('defect-photo.jpg', 'rb') as f:
resp = requests.put(upload['url'], headers=upload['headers'], data=f)
# resp.status_code should be 200

Ruby

require 'net/http'

uri = URI(upload['url'])
req = Net::HTTP::Put.new(uri)
upload['headers'].each { |k, v| req[k] = v }
req.body = File.binread('defect-photo.jpg')

resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
# resp.code should be '200'

Note: Files are scanned for malware automatically after upload. If malware is detected, the attachment is quarantined.

Step 3: Reference attachments when creating a record

Pass the key values from Step 1 in the attachmentKeys array when creating the record.

Example: Creating a ticket with attachments

POST https://app.visibuild.com.au/api/core/v1/tickets
Authorization: Bearer {access_token}
Content-Type: application/json

{
"title": "Kitchen cabinet door hinge broken",
"projectId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"address": "Unit 405, 123 Main St",
"priority": "medium",
"attachmentKeys": [
"attachment/abc123/defect-photo.jpg",
"attachment/def456/floor-plan.pdf"
],
"contact": {
"name": "Jane Doe",
"email": "jane.doe@example.com"
}
}

The ticket is created with the attachments linked. You can include as many keys as needed in the array.

Validation

When attachmentKeys is provided, the server validates each key before creating the record:

Check

Error on failure

Key exists in S3.

422: attachment key not found.

File belongs to the same company.

422: attachment belongs to another company.

File is not already linked to another record.

422: attachment already associated.

If any key fails validation, the entire request is rejected and no record is created.

Uploading multiple files

You can upload several files in parallel by making multiple POST /attachments requests at the same time. Collect all the returned keys, then pass them together in attachmentKeys when creating the record.

There is no separate step to "finalise" the uploads. The association happens when the record is created.

Troubleshooting

Problem

Cause

Solution

S3 upload returns 403.

Presigned URL has expired or headers don't match.

Request a new presigned URL and retry. Ensure Content-Type and Content-MD5 match exactly.

422 on record creation: key not found.

File was not uploaded to S3, or the presigned URL expired before upload completed.

Verify the S3 upload succeeded (HTTP 200) before referencing the key.

422 on record creation: already associated.

The same attachment key was used in a previous request.

Each uploaded file can only be linked to one record. Upload a new copy if needed.

For the full endpoint reference, see the Core API documentation.

Did this answer your question?