Media Upload 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
| Method | Path | Description |
|---|---|---|
| POST | /uploads | Single-request upload (multipart) or create chunked stream (JSON) |
| PATCH | /uploads/:id | Send 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:
| Destination | Max Size | Formats | Description |
|---|---|---|---|
stepImage | 8 MB | JPEG, PNG | Step instruction images |
stepVideo | 500 MB | MP4 | Step instruction videos |
stepVideoGif | 150 MB | GIF | Animated 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
| Status | Meaning |
|---|---|
| 400 | Bad request (missing fields, invalid destination, SHA mismatch) |
| 404 | Stream not found (chunked upload) |
| 413 | File too large for destination |
| 500 | Internal 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