Files & media
One upload API — reserve a presigned PUT, send the bytes, poll readiness. URLs are references on posts; files are a first-class resource with get, list, and delete.
Media in Inklate follows three rules:
- There is exactly one upload API. Reserve a presigned upload for a local file, PUT the raw bytes to the returned URL, then poll the file until it’s ready. There is no inline base64 upload and no “complete” call — a server-side watcher finalizes the moment the bytes land.
- Files already hosted at a public URL are referenced, not uploaded. Put the
https://URL straight into a post’smedialist; the server fetches, verifies, and stores it, and the response tells you the file id to poll. - Uploaded objects are a
filesresource. Read one atGET /files/{fileId}, list them atGET /files, delete atDELETE /files/{fileId}. Upload is the action; files are the nouns.
Supported types and limits
| Kind | MIME types | Max size |
|---|---|---|
| Image | image/jpeg, image/png, image/gif, image/webp | 25 MB |
| Video | video/mp4, video/quicktime | 512 MB |
A declared type outside this table fails the upload call immediately with error.reason: "unsupported_type". Bytes that sniff to a different type than declared fail with content_mismatch — the declared MIME is never trusted. WebM is not accepted (its container can’t be probed for the facts per-channel checks need).
Time-to-live values: the presigned PUT URL lasts 3600 s, a signed download URL lasts 600 s, and an unfilled reservation expires after 2 hours (then it’s garbage-collected).
Upload flow (local files)
media.upload → PUT each file’s bytes → poll files.get until readiness leaves "processing". The same contract on REST, MCP, and tRPC.
# 1. Reserve — 1–10 files per call, all-or-nothing.
curl -X POST https://api.inklate.com/api/v1/media/upload \
-H "Authorization: Bearer inklate_XXXXXXXX" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: launch-carousel-1" \
-d '{ "files": [
{ "mime_type": "image/jpeg", "bytes": 812340 },
{ "mime_type": "video/mp4", "bytes": 31457280 }
] }'
# → { "files": [ { "file": { "id": "file_…", "readiness": "processing", … },
# "upload": { "url": "https://…", "headers": { "Content-Type": "image/jpeg" },
# "expires_in_seconds": 3600 } }, … ],
# "replayed": false }
# 2. PUT the raw bytes with the returned Content-Type. No complete call.
curl -T photo.jpg -H "Content-Type: image/jpeg" "https://…presigned…"
# 3. Poll until readiness leaves "processing".
curl -H "Authorization: Bearer inklate_XXXXXXXX" \
https://api.inklate.com/api/v1/files/file_abcBatch semantics: an invalid entry fails the whole call with the offending index ({ "error": { "code": "BAD_REQUEST", "reason": "unsupported_type", "index": 1, … } }) and nothing is created. After creation the files are fully independent — each has its own PUT URL and its own finalize watcher; one failed PUT never affects its siblings.
Send an Idempotency-Key header (or idempotency_key in the body): a retried call with the same key returns the original reservations, with fresh PUT URLs for files whose bytes haven’t landed yet ("replayed": true).
Poll a whole batch in one round-trip with GET /files?ids=file_a,file_b,file_c.
URL references (files hosted elsewhere)
Don’t download-and-reupload a file that already has a public URL — reference it on the post:
curl -X POST https://api.inklate.com/api/v1/posts \
-H "Authorization: Bearer inklate_XXXXXXXX" \
-H "Content-Type: application/json" \
-d '{
"body": "Launch day.",
"channels": ["chn_…"],
"media": ["file_9f2…", "https://cdn.example.com/launch.mp4"]
}'
# → { …post…, "media": [
# { "fileId": "file_9f2…", "sourceUrl": null },
# { "fileId": "file_new…", "sourceUrl": "https://cdn.example.com/launch.mp4" } ] }The media list mixes file ids and https:// URLs in render order. Each URL is ingested server-side (https-only, public hosts only, verified against the same type/size table) and becomes a normal file; the response echoes the url → fileId mapping so you can poll each one to readiness. This is how chat-hosted agents — which can’t PUT raw bytes — attach video.
The state machine
The raw status walks pending_upload → uploaded → processing → ready | failed | rejected, with a parallel scan verdict. Act on readiness, the server-folded 3-bucket field:
"processing"— keep polling."ready"— publishable;include_url=truenow returns a signed download URL."failed"— terminal; readerror.reason, fix, re-upload.
A post can be created while an attachment is still processing; scheduling/publishing waits for ready.
Error reasons
Failures carry a machine slug beside the human message — in the file’s error.reason, the REST error envelope, and the MCP error text ([reason:…]).
| Reason | Meaning | Do |
|---|---|---|
too_large | Bytes exceed the per-kind ceiling | Compress or trim, re-upload |
unsupported_type | Type not in the supported table | Convert (e.g. WebM → MP4) |
content_mismatch | Bytes sniff to a different type than declared | Declare the real type |
empty | Zero bytes landed | Re-upload the real file |
upload_incomplete | The PUT never finished before the reservation expired | Reserve again, PUT again |
fetch_failed | A URL reference couldn’t be fetched (or was refused) | Check the URL is public https |
decode_failed | The bytes aren’t a decodable image/video | Re-encode the file |
infected | Failed the virus scan | Don’t attach it |
The files resource
GET /files/{fileId}?include_url=true&variant=thumbnail— one file;include_urladds{ url, url_expires_in_seconds }once ready (variantsigns a named derivative instead of the original).GET /files?limit=20&cursor=…&readiness=ready&kind=image&ids=…— newest first, keyset-paginated vianext_cursor.DELETE /files/{fileId}— permanent and irreversible; refused with409while the file is attached to a post (update the post’smedialist first).
The MCP tools mirror this one-to-one: files_upload, files_get, files_list, files_delete — with the caveat that files_upload needs a runtime that can PUT raw bytes (CLI agents); chat-hosted agents use URL references on posts_create instead.
Errors and fixes
The machine catalog — HTTP statuses, reason slugs, per-channel validation codes, the five fix kinds with their parameters, and the upgrade markers — everything an agent branches on.
API keys
Create an Organization API key in the dashboard under Settings → API, copy it once, and send it as a bearer token to MCP or REST.