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
configImage10 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"

configImage
------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=configImage" \
-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="configImage"):
"""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 10MB"
}

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=configImage" \
-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