Skip to main content

Media Upload API

Alpha API

This API is in active development and may change without notice. Authentication requirements and endpoint locations are subject to change.

The Media Upload API provides HTTP endpoints for uploading images and videos to be used in work instructions. It supports both single-request uploads (for small files) and chunked uploads (for large files).

Endpoints

MethodPathDescription
POST/uploadsSingle-request upload (multipart) or create chunked stream (JSON)
PATCH/uploads/:idSend chunk to existing stream

Base URL: https://example.picomes.io

Authentication

All requests require the x-pico-api-org header with your API token:

x-pico-api-org: YOUR_API_TOKEN

See the Authentication guide for details on obtaining an API token.

Destinations

Each upload must specify a destination that determines file type restrictions and processing:

DestinationMax SizeFormatsDescription
stepImage8 MBJPEG, PNGStep instruction images
stepVideo500 MBMP4Step instruction videos
stepVideoGif150 MBGIFAnimated step instructions

Single-Request Upload

For files under 10 MB, use a single multipart POST request.

Request

POST /uploads HTTP/1.1
Content-Type: multipart/form-data; boundary=----FormBoundary

------FormBoundary
Content-Disposition: form-data; name="destinationId"

stepImage
------FormBoundary
Content-Disposition: form-data; name="file"; filename="step1.jpg"
Content-Type: image/jpeg

<binary image data>
------FormBoundary--

Response

{
"path": "/api/v1/images/abc123-step1.jpg"
}

cURL Example

curl -X POST https://example.picomes.io/uploads \
-H "x-pico-api-org: YOUR_API_TOKEN" \
-F "destinationId=stepImage" \
-F "file=@/path/to/image.jpg"

Python Example

import json
import urllib.request
import os

UPLOAD_ENDPOINT = "https://example.picomes.io/uploads"
API_TOKEN = "YOUR_API_TOKEN"

def upload_image(file_path, destination_id="stepImage"):
"""Upload an image and return the resulting path."""
import mimetypes

boundary = "----PythonFormBoundary7MA4YWxkTrZu0gW"
filename = os.path.basename(file_path)
content_type = mimetypes.guess_type(file_path)[0] or "application/octet-stream"

with open(file_path, "rb") as f:
file_data = f.read()

# Build multipart form data
body = []

# destinationId field
body.append(f"--{boundary}".encode())
body.append(b'Content-Disposition: form-data; name="destinationId"')
body.append(b"")
body.append(destination_id.encode())

# file field
body.append(f"--{boundary}".encode())
body.append(f'Content-Disposition: form-data; name="file"; filename="{filename}"'.encode())
body.append(f"Content-Type: {content_type}".encode())
body.append(b"")
body.append(file_data)

body.append(f"--{boundary}--".encode())
body.append(b"")

payload = b"\r\n".join(body)

req = urllib.request.Request(
UPLOAD_ENDPOINT,
data=payload,
headers={
"Content-Type": f"multipart/form-data; boundary={boundary}",
"x-pico-api-org": API_TOKEN
}
)

with urllib.request.urlopen(req) as resp:
data = json.loads(resp.read().decode())

return data.get("path", "")

# Usage
path = upload_image("/path/to/step_image.jpg")
print(f"Uploaded to: {path}")

Chunked Upload

For large files (videos), use chunked upload to avoid timeouts and memory issues.

Step 1: Create Stream

POST /uploads HTTP/1.1
Content-Type: application/json

{
"destinationId": "stepVideo",
"fileName": "assembly.mp4",
"fileSize": 52428800,
"sha": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}

Response:

{
"streamId": "abc123xyz"
}

Step 2: Send Chunks

PATCH /uploads/abc123xyz HTTP/1.1
Content-Type: multipart/form-data; boundary=----FormBoundary

------FormBoundary
Content-Disposition: form-data; name="chunkIndex"

0
------FormBoundary
Content-Disposition: form-data; name="totalChunks"

5
------FormBoundary
Content-Disposition: form-data; name="file"; filename="chunk.bin"
Content-Type: application/octet-stream

<binary chunk data>
------FormBoundary--

Intermediate Response:

{
"streamId": "abc123xyz",
"chunkIndex": 0
}

Final Chunk Response:

{
"path": "/api/v1/videos/abc123xyz-assembly.mp4"
}

Python Example (Chunked)

import hashlib
import json
import urllib.request
import os

UPLOAD_ENDPOINT = "https://example.picomes.io/uploads"
API_TOKEN = "YOUR_API_TOKEN"
CHUNK_SIZE = 5 * 1024 * 1024 # 5 MB chunks

def upload_video_chunked(file_path, destination_id="stepVideo"):
"""Upload a large video file in chunks."""
filename = os.path.basename(file_path)
file_size = os.path.getsize(file_path)

# Calculate SHA-256
sha256 = hashlib.sha256()
with open(file_path, "rb") as f:
for chunk in iter(lambda: f.read(8192), b""):
sha256.update(chunk)
file_sha = sha256.hexdigest()

# Create stream
create_payload = json.dumps({
"destinationId": destination_id,
"fileName": filename,
"fileSize": file_size,
"sha": file_sha
}).encode()

req = urllib.request.Request(
UPLOAD_ENDPOINT,
data=create_payload,
headers={
"Content-Type": "application/json",
"x-pico-api-org": API_TOKEN
}
)

with urllib.request.urlopen(req) as resp:
stream_data = json.loads(resp.read().decode())

stream_id = stream_data["streamId"]

# Calculate total chunks
total_chunks = (file_size + CHUNK_SIZE - 1) // CHUNK_SIZE

# Send chunks
with open(file_path, "rb") as f:
for chunk_index in range(total_chunks):
chunk_data = f.read(CHUNK_SIZE)

boundary = "----PythonChunkBoundary"
body = []

body.append(f"--{boundary}".encode())
body.append(b'Content-Disposition: form-data; name="chunkIndex"')
body.append(b"")
body.append(str(chunk_index).encode())

body.append(f"--{boundary}".encode())
body.append(b'Content-Disposition: form-data; name="totalChunks"')
body.append(b"")
body.append(str(total_chunks).encode())

body.append(f"--{boundary}".encode())
body.append(b'Content-Disposition: form-data; name="file"; filename="chunk.bin"')
body.append(b"Content-Type: application/octet-stream")
body.append(b"")
body.append(chunk_data)

body.append(f"--{boundary}--".encode())
body.append(b"")

payload = b"\r\n".join(body)

req = urllib.request.Request(
f"{UPLOAD_ENDPOINT}/{stream_id}",
data=payload,
headers={
"Content-Type": f"multipart/form-data; boundary={boundary}",
"x-pico-api-org": API_TOKEN
},
method="PATCH"
)

with urllib.request.urlopen(req) as resp:
result = json.loads(resp.read().decode())

print(f"Chunk {chunk_index + 1}/{total_chunks} uploaded")

if "path" in result:
return result["path"]

return None

Using Uploaded Media in Work Instructions

Once uploaded, use the returned path in your process definitions:

mutation CreateProcess($input: ProcessCreateInput!) {
processCreate(input: $input)
}
{
"input": {
"process": {
"id": "my_process",
"name": "Assembly Process",
"steps": [
{
"id": "step_1",
"name": "First Step",
"instructionText": {"text": "Attach the component"},
"primaryImage": {
"path": "/api/v1/images/abc123-step1.jpg"
},
"actions": [
{
"id": "action_1",
"description": {"text": "Align and secure"}
}
]
}
]
}
}
}

Error Responses

StatusMeaning
400Bad request (missing fields, invalid destination, SHA mismatch)
404Stream not found (chunked upload)
413File too large for destination
500Internal server error

Example Error Response

{
"error": "file of size 15MB is larger than 8MB"
}

SHA Verification

For chunked uploads, the SHA-256 hash provided at stream creation is verified when the final chunk is received. If the hash doesn't match, the upload fails with a 400 error.

For single-request uploads, SHA verification is optional — include a sha form field to enable it:

curl -X POST https://example.picomes.io/uploads \
-H "x-pico-api-org: YOUR_API_TOKEN" \
-F "destinationId=stepImage" \
-F "sha=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" \
-F "file=@/path/to/image.jpg"

Limitations

  • Maximum single-request upload size is 10 MB (Envoy buffer limit)
  • Larger files must use chunked upload
  • Uploaded files are not automatically cleaned up if not used