Appearance
API Reference — Media
Upload the bytes of an image a lead sent you and get back a permanent URL to pass to POST /chat as image_url. Use this when your platform hands you a file rather than a link you can share — a Telegram file_id you download with your bot token, or an Instagram CDN URL that is signed and expires.
The endpoint is under the production base URL:
https://api-talk.fluidvip.com/api/v1/charactersYou do not need this if you already have a public URL.
image_urlon/chattakes any link that resolves to the image bytes — your own CDN, an S3/R2 object, the platform's own permanent media URL. This endpoint exists for the case where you cannot hand us a fetchable link. See Sending images.
Authentication. Same per-character connector token as every other call:
X-Connector-Token: ftc_live_.... See Authentication.
POST /inbound-media
Bytes in, permanent URL out.
Request body
| Name | Type | Required | Description |
|---|---|---|---|
platform | string | Yes | The platform this media arrived on, e.g. instagram. |
data_b64 | string | Yes | The raw image bytes, base64-encoded. Max 10 MB decoded. |
filename | string | No | Original filename, used for the stored file's name. Defaults to a generated .jpg name. |
content_type | string | No | MIME type of the bytes. Defaults to image/jpeg. |
The upload is not idempotent — each call stores a new file and returns a new URL. Upload once per inbound photo.
Response
| Field | Type | Description |
|---|---|---|
url | string | The permanent, publicly-fetchable URL. Pass it straight to /chat as image_url. |
file_id | string | The stored file's id, for your own bookkeeping. |
json
{
"data": {
"url": "https://cloud-files.fluidvip.com/s/9a3f...",
"file_id": "file_7c0d2e1f8a"
},
"request_id": "req_4b1a9d7e22"
}The file lands in the character owner's storage, in a folder named after the character ("<name> — Inbound"), separate from the character's own photo vault. The returned URL carries no platform token, so nothing of yours leaks into it and it does not expire the way a platform CDN link does.
The two-step flow
bash
# 1. bytes in -> permanent url out
URL=$(curl -s -X POST https://api-talk.fluidvip.com/api/v1/characters/inbound-media \
-H "X-Connector-Token: ftc_live_..." \
-H "Content-Type: application/json" \
-d "{\"platform\":\"instagram\",\"data_b64\":\"$(base64 -w0 photo.jpg)\",\"filename\":\"photo.jpg\"}" \
| python -c "import sys,json; print(json.load(sys.stdin)['data']['url'])")
# 2. attach it to the turn
curl -X POST https://api-talk.fluidvip.com/api/v1/characters/chat \
-H "X-Connector-Token: ftc_live_..." \
-H "Content-Type: application/json" \
-d "{\"platform\":\"instagram\",\"handle\":\"mark\",\"message\":\"what do you think? 😏\",\"image_url\":\"$URL\"}"python
import base64, requests
BASE = "https://api-talk.fluidvip.com/api/v1/characters"
H = {"X-Connector-Token": "ftc_live_..."}
# 1. upload the bytes your connector already downloaded from the platform
up = requests.post(f"{BASE}/inbound-media", headers=H, json={
"platform": "instagram",
"data_b64": base64.b64encode(raw_bytes).decode(),
"filename": "photo.jpg",
"content_type": "image/jpeg",
}).json()["data"]
# 2. hand the URL to the turn
reply = requests.post(f"{BASE}/chat", headers=H, json={
"platform": "instagram",
"handle": "mark",
"message": "what do you think? 😏",
"image_url": up["url"],
}).json()["data"]typescript
const BASE = "https://api-talk.fluidvip.com/api/v1/characters";
const H = { "X-Connector-Token": "ftc_live_...", "Content-Type": "application/json" };
// 1. upload the bytes your connector already downloaded from the platform
const up = await fetch(`${BASE}/inbound-media`, {
method: "POST",
headers: H,
body: JSON.stringify({
platform: "instagram",
data_b64: Buffer.from(rawBytes).toString("base64"),
filename: "photo.jpg",
content_type: "image/jpeg",
}),
}).then((r) => r.json());
// 2. hand the URL to the turn
const reply = await fetch(`${BASE}/chat`, {
method: "POST",
headers: H,
body: JSON.stringify({
platform: "instagram",
handle: "mark",
message: "what do you think? 😏",
image_url: up.data.url,
}),
}).then((r) => r.json());In the SDKs since 2.1.0. Both clients wrap this as
inbound_media/inboundMediaand take raw bytes — the base64 is done for you. Upgrade withpip install -U fluidtalkornpm install fluidtalk@latest.
python
up = ft.inbound_media(platform="instagram", data=raw_bytes, filename="photo.jpg")
ft.chat(platform="instagram", handle="mark", message="what do you think? 😏", image_url=up.url)typescript
const up = await ft.inboundMedia({ platform: "instagram", data: rawBytes, filename: "photo.jpg" });
await ft.chat({ platform: "instagram", handle: "mark", message: "what do you think? 😏", imageUrl: up.url });Errors
| Status | error.code | When |
|---|---|---|
400 | invalid_request | data_b64 is missing or is not valid base64. |
401 | invalid_token | Missing, invalid, or revoked X-Connector-Token. |
403 | forbidden | The token isn't allowed to act on this platform. |
404 | not_found | The token's character no longer exists. |
413 | payload_too_large | The decoded bytes exceed 10 MB. Downscale before uploading. |
422 | validation_error | The body is well-formed JSON but a field failed validation. |
502 | upstream_error | The upload to storage failed. Retry with backoff. |
503 | service_unavailable | Storage is not configured. Retry later; don't retry in a tight loop. |
A failed upload should not block the conversation. Fall back to sending the turn as text — either the lead's caption alone, or a short placeholder like "[he sent you a photo]" — so the character still answers.
Related
- Chat —
POST /chatand theimage_urlfield this URL feeds. - Sending & receiving DMs — the full inbound loop, including images.
- API Reference index