ChatKomodo Docs Home Sign in

API reference · Messaging

Media

Upload images, videos, audio and documents once and send them by key (media_r2_key), or send straight from a public url.

Reserve an upload key and URL

POST/v1/media/upload-url

Reserves a media key for the caller and returns the URL to upload the bytes to with PUT (same Authorization header). Use the returned r2_key as media_r2_key when sending. The body is optional; the extension of the key comes from filename (bin when absent).

Request body

  • filenamestring
    Original file name, used for its extension and for display
  • content_typestring
    MIME type, e.g. image/jpeg

Responses

  • 200Reserved
    Response fields
    • r2_keystringrequired
      uploads/<32 hex>.<ext>: the value to use as media_r2_key
    • upload_urlstringrequired
      Absolute URL for PUT /v1/media/put/{key}
    • methodstringrequired
      One of: PUT
    • expires_atintegerrequired
      One hour from now, Unix ms
    • content_typestring
      Echo of the request
    • max_bytesintegerrequired
      Handler limit (25 MB); the global request limit of 10 MB applies first
  • 401Missing, invalid or expired credentials
Request
curl -X POST "https://api.chatkomodo.com/v1/media/upload-url" \
  -H "Authorization: Bearer $CHATKOMODO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "filename": "price-list.pdf",
  "content_type": "application/pdf"
}'
const res = await fetch('https://api.chatkomodo.com/v1/media/upload-url', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CHATKOMODO_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "filename": "price-list.pdf",
    "content_type": "application/pdf"
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.code}: ${data.error}`);
console.log(data);
import os
import requests

res = requests.post(
    "https://api.chatkomodo.com/v1/media/upload-url",
    headers={
        "Authorization": f"Bearer {os.environ['CHATKOMODO_API_KEY']}",
    },
    json={
        "filename": "price-list.pdf",
        "content_type": "application/pdf",
    },
    timeout=30,
)
res.raise_for_status()
print(res.json())
<?php
require 'vendor/autoload.php';

$client = new GuzzleHttp\Client();
$res = $client->request('POST', 'https://api.chatkomodo.com/v1/media/upload-url', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('CHATKOMODO_API_KEY'),
    ],
    'json' => [
        'filename' => 'price-list.pdf',
        'content_type' => 'application/pdf',
    ],
]);

print_r(json_decode((string) $res->getBody(), true));
Response 200
{
  "r2_key": "uploads/abc123.jpg",
  "upload_url": "https://example.com/file.jpg",
  "method": "PUT",
  "expires_at": 1767225600000,
  "content_type": "application/pdf",
  "max_bytes": 1
}

Upload bytes to a reserved key

PUT/v1/media/put/{key}

Stores the request body as the media file for a key reserved with POST /v1/media/upload-url. The Content-Type header is recorded as the file type. Only keys of the form uploads/<32 hex>.<ext> are accepted, and a key reserved by another tenant is refused.

Path parameters

  • keystringrequired
    Media key as returned by the upload, uploads/<32 hex>.<ext>. The slash is part of the path (not percent-encoded)

Request body

string (binary)

Responses

  • 200Stored
    Response fields
    • okbooleanrequired
    • sizeintegerrequired
      Bytes stored
  • 400The key does not have the form uploads/<32 hex>.<ext> ({ error: "Invalid key" }, no code)
  • 401Missing, invalid or expired credentials
  • 404The key is reserved by another tenant (no code)
  • 413The file is larger than 25 MB (handler limit), or larger than the global 10 MB request limit, which applies first
Request
curl -X PUT "https://api.chatkomodo.com/v1/media/put/uploads%2Fabc123.jpg" \
  -H "Authorization: Bearer $CHATKOMODO_API_KEY"
const res = await fetch('https://api.chatkomodo.com/v1/media/put/uploads%2Fabc123.jpg', {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${process.env.CHATKOMODO_API_KEY}`,
  },
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.code}: ${data.error}`);
console.log(data);
import os
import requests

res = requests.put(
    "https://api.chatkomodo.com/v1/media/put/uploads%2Fabc123.jpg",
    headers={
        "Authorization": f"Bearer {os.environ['CHATKOMODO_API_KEY']}",
    },
    timeout=30,
)
res.raise_for_status()
print(res.json())
<?php
require 'vendor/autoload.php';

$client = new GuzzleHttp\Client();
$res = $client->request('PUT', 'https://api.chatkomodo.com/v1/media/put/uploads%2Fabc123.jpg', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('CHATKOMODO_API_KEY'),
    ],
]);

print_r(json_decode((string) $res->getBody(), true));
Response 200
{
  "ok": true,
  "size": 12
}

Upload a file (multipart)

POST/v1/media/upload

Uploads one file in a single multipart request and returns its media key, to use as media_r2_key. The key is generated by the server; the extension comes from the file name.

Request body (multipart/form-data)

  • filestring (binary)required
    The file to upload, in the form field file

Responses

  • 201Uploaded
    Response fields
    • keystringrequired
      uploads/<32 hex>.<ext>
    • sizeintegerrequired
      Bytes stored
  • 400No file field in the form (no code)
  • 401Missing, invalid or expired credentials
  • 413The file is larger than 25 MB (handler limit), or larger than the global 10 MB request limit, which applies first
Request
curl -X POST "https://api.chatkomodo.com/v1/media/upload" \
  -H "Authorization: Bearer $CHATKOMODO_API_KEY" \
  -F "[email protected]"
import { readFile } from 'node:fs/promises';

const form = new FormData();
form.append('file', new Blob([await readFile('price-list.pdf')]), 'price-list.pdf');

const res = await fetch('https://api.chatkomodo.com/v1/media/upload', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CHATKOMODO_API_KEY}`,
  },
  body: form,
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.code}: ${data.error}`);
console.log(data);
import os
import requests

res = requests.post(
    "https://api.chatkomodo.com/v1/media/upload",
    headers={
        "Authorization": f"Bearer {os.environ['CHATKOMODO_API_KEY']}",
    },
    files={"file": open("price-list.pdf", "rb")},
    timeout=30,
)
res.raise_for_status()
print(res.json())
<?php
require 'vendor/autoload.php';

$client = new GuzzleHttp\Client();
$res = $client->request('POST', 'https://api.chatkomodo.com/v1/media/upload', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('CHATKOMODO_API_KEY'),
    ],
    'multipart' => [
        ['name' => 'file', 'contents' => fopen('price-list.pdf', 'r')],
    ],
]);

print_r(json_decode((string) $res->getBody(), true));
Response 201
{
  "key": "uploads/abc123.jpg",
  "size": 12
}

List uploaded media

GET/v1/media

Completed uploads of the tenant (reserved keys that have no bytes yet are left out), newest first.

Query parameters

  • limitinteger
    Values outside 1-500 are clamped
    Default: 200From 1 to 500

Responses

  • 200Media
    Response fields
    • itemsarray of objectrequired
    • items[].keystringrequired
    • items[].filenamestring | nullrequired
    • items[].content_typestring | nullrequired
    • items[].sizeintegerrequired
      Bytes
    • items[].created_atintegerrequired
      Unix time in milliseconds
  • 401Missing, invalid or expired credentials
Request
curl "https://api.chatkomodo.com/v1/media" \
  -H "Authorization: Bearer $CHATKOMODO_API_KEY"
const res = await fetch('https://api.chatkomodo.com/v1/media', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.CHATKOMODO_API_KEY}`,
  },
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.code}: ${data.error}`);
console.log(data);
import os
import requests

res = requests.get(
    "https://api.chatkomodo.com/v1/media",
    headers={
        "Authorization": f"Bearer {os.environ['CHATKOMODO_API_KEY']}",
    },
    timeout=30,
)
res.raise_for_status()
print(res.json())
<?php
require 'vendor/autoload.php';

$client = new GuzzleHttp\Client();
$res = $client->request('GET', 'https://api.chatkomodo.com/v1/media', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('CHATKOMODO_API_KEY'),
    ],
]);

print_r(json_decode((string) $res->getBody(), true));
Response 200
{
  "items": [
    {
      "key": "uploads/abc123.jpg",
      "filename": "price-list.pdf",
      "content_type": "application/pdf",
      "size": 12,
      "created_at": 1767225600000
    }
  ]
}

Download a media file

GET/v1/media/{key}

Returns the file bytes. Only the owning tenant can read a file. The Content-Type is the recorded type for images (png, jpeg, gif, webp), video (mp4, 3gpp, quicktime, webm), audio and PDF, and application/octet-stream for anything else. Cached privately for 5 minutes.

Path parameters

  • keystringrequired
    Media key as returned by the upload, uploads/<32 hex>.<ext>. The slash is part of the path (not percent-encoded)

Responses

  • 200File bytes
  • 400The key is not a valid media key ({ error: "Invalid key" }, no code)
  • 401Missing, invalid or expired credentials
  • 404No such file, or it belongs to another tenant (no code)
Request
curl "https://api.chatkomodo.com/v1/media/uploads%2Fabc123.jpg" \
  -H "Authorization: Bearer $CHATKOMODO_API_KEY"
const res = await fetch('https://api.chatkomodo.com/v1/media/uploads%2Fabc123.jpg', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.CHATKOMODO_API_KEY}`,
  },
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.code}: ${data.error}`);
console.log(data);
import os
import requests

res = requests.get(
    "https://api.chatkomodo.com/v1/media/uploads%2Fabc123.jpg",
    headers={
        "Authorization": f"Bearer {os.environ['CHATKOMODO_API_KEY']}",
    },
    timeout=30,
)
res.raise_for_status()
print(res.json())
<?php
require 'vendor/autoload.php';

$client = new GuzzleHttp\Client();
$res = $client->request('GET', 'https://api.chatkomodo.com/v1/media/uploads%2Fabc123.jpg', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('CHATKOMODO_API_KEY'),
    ],
]);

print_r(json_decode((string) $res->getBody(), true));
Response 200

application/octet-stream, image/*, video/*, audio/*, application/pdf — the file itself.

Delete a media file

DEL/v1/media/{key}

Deletes the file and its record. Only the owning tenant can delete it. Message templates or recurring messages that still reference the key will no longer find the file.

Path parameters

  • keystringrequired
    Media key as returned by the upload, uploads/<32 hex>.<ext>. The slash is part of the path (not percent-encoded)

Responses

  • 200Deleted
    Response fields
    • okbooleanrequired
  • 400The key is not a valid media key ({ error: "Invalid key" }, no code)
  • 401Missing, invalid or expired credentials
  • 403Forbidden for a customer-service agent login (Agent access is not allowed for this operation)
  • 404No such file, or it belongs to another tenant (no code)
  • 500The file could not be removed from storage (no code)
Request
curl -X DELETE "https://api.chatkomodo.com/v1/media/uploads%2Fabc123.jpg" \
  -H "Authorization: Bearer $CHATKOMODO_API_KEY"
const res = await fetch('https://api.chatkomodo.com/v1/media/uploads%2Fabc123.jpg', {
  method: 'DELETE',
  headers: {
    Authorization: `Bearer ${process.env.CHATKOMODO_API_KEY}`,
  },
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.code}: ${data.error}`);
console.log(data);
import os
import requests

res = requests.delete(
    "https://api.chatkomodo.com/v1/media/uploads%2Fabc123.jpg",
    headers={
        "Authorization": f"Bearer {os.environ['CHATKOMODO_API_KEY']}",
    },
    timeout=30,
)
res.raise_for_status()
print(res.json())
<?php
require 'vendor/autoload.php';

$client = new GuzzleHttp\Client();
$res = $client->request('DELETE', 'https://api.chatkomodo.com/v1/media/uploads%2Fabc123.jpg', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('CHATKOMODO_API_KEY'),
    ],
]);

print_r(json_decode((string) $res->getBody(), true));
Response 200
{
  "ok": true
}