. Create an API key under API keys.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
/v1/media/upload-urlReserves 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
filenamestringOriginal file name, used for its extension and for displaycontent_typestringMIME type, e.g.image/jpeg
Responses
- 200Reserved
Response fields
r2_keystringrequireduploads/<32 hex>.<ext>: the value to use asmedia_r2_keyupload_urlstringrequiredAbsolute URL forPUT /v1/media/put/{key}methodstringrequiredexpires_atintegerrequiredOne hour from now, Unix mscontent_typestringEcho of the requestmax_bytesintegerrequiredHandler limit (25 MB); the global request limit of 10 MB applies first
- 401Missing, invalid or expired credentials
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));{
"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
/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
keystringrequiredMedia 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
okbooleanrequiredsizeintegerrequiredBytes stored
- 400The key does not have the form
uploads/<32 hex>.<ext>({ error: "Invalid key" }, nocode) - 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
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));{
"ok": true,
"size": 12
}Upload a file (multipart)
/v1/media/uploadUploads 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)requiredThe file to upload, in the form fieldfile
Responses
- 201Uploaded
Response fields
keystringrequireduploads/<32 hex>.<ext>sizeintegerrequiredBytes stored
- 400No
filefield in the form (nocode) - 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
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));{
"key": "uploads/abc123.jpg",
"size": 12
}List uploaded media
/v1/mediaCompleted uploads of the tenant (reserved keys that have no bytes yet are left out), newest first.
Query parameters
limitintegerValues outside 1-500 are clamped
Responses
- 200Media
Response fields
itemsarray of objectrequireditems[].keystringrequireditems[].filenamestring | nullrequireditems[].content_typestring | nullrequireditems[].sizeintegerrequiredBytesitems[].created_atintegerrequiredUnix time in milliseconds
- 401Missing, invalid or expired credentials
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));{
"items": [
{
"key": "uploads/abc123.jpg",
"filename": "price-list.pdf",
"content_type": "application/pdf",
"size": 12,
"created_at": 1767225600000
}
]
}Download a media file
/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
keystringrequiredMedia 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" }, nocode) - 401Missing, invalid or expired credentials
- 404No such file, or it belongs to another tenant (no
code)
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));application/octet-stream, image/*, video/*, audio/*, application/pdf — the file itself.
Delete a media file
/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
keystringrequiredMedia 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" }, nocode) - 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)
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));{
"ok": true
}