Skip to content

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/characters

You do not need this if you already have a public URL. image_url on /chat takes 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

NameTypeRequiredDescription
platformstringYesThe platform this media arrived on, e.g. instagram.
data_b64stringYesThe raw image bytes, base64-encoded. Max 10 MB decoded.
filenamestringNoOriginal filename, used for the stored file's name. Defaults to a generated .jpg name.
content_typestringNoMIME 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

FieldTypeDescription
urlstringThe permanent, publicly-fetchable URL. Pass it straight to /chat as image_url.
file_idstringThe 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 / inboundMedia and take raw bytes — the base64 is done for you. Upgrade with pip install -U fluidtalk or npm 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

Statuserror.codeWhen
400invalid_requestdata_b64 is missing or is not valid base64.
401invalid_tokenMissing, invalid, or revoked X-Connector-Token.
403forbiddenThe token isn't allowed to act on this platform.
404not_foundThe token's character no longer exists.
413payload_too_largeThe decoded bytes exceed 10 MB. Downscale before uploading.
422validation_errorThe body is well-formed JSON but a field failed validation.
502upstream_errorThe upload to storage failed. Retry with backoff.
503service_unavailableStorage 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.


FluidTalk Characters API — part of the Fluidvip ecosystem.