API v2 (beta)
Media
The Media directory holds your workspace's files: price lists, menus, brochures, product photos. Upload a file once, then send it as often as you like with "media": {"media_id": …} in a message or a reminder. It's the same directory as the Media directory page in the dashboard, so files uploaded there are listed here too.
| Operation | Endpoint | Scope |
|---|---|---|
| Upload a file | POST /api/v2/media | media |
| List files | GET /api/v2/media | media |
| Get a file | GET /api/v2/media/{id} | media |
| Delete a file | DELETE /api/v2/media/{id} | media |
Upload a file#
https://wbiztool.com/api/v2/mediaBody: multipart/form-data
Send one file per request as multipart/form-data, in the field file. This is the one v2 endpoint that doesn't take JSON. The answer is 201 with the Media object.
- Up to 64 MB per file.
- Types: the type comes from the extension at the end of the file name, so keep it.
| Kind | Extensions |
|---|---|
| Images | .jpg, .jpeg, .jpe, .png, .gif, .webp, .bmp |
| Documents | .pdf, .doc, .docx, .xls, .xlsx, .ppt, .pptx, .csv, .txt |
| Audio | .mp3, .m4a, .wav, .ogg, .oga, .aac |
| Video | .mp4, .mov, .avi |
| Archives | .zip, .rar |
- Storage: all the workspace's files together can take up what its plan allows, from 250 MB to 10 GB (see Storage per plan). A file that doesn't fit is refused with
storage_full. - An image file (
typeimage) is sent as an image by default; anything else as a file. - Send an
Idempotency-Keyso a retry after a timeout doesn't store the file twice.
curl -X POST https://wbiztool.com/api/v2/media \
-H "Authorization: Bearer $WBIZTOOL_API_KEY" \
-H "Idempotency-Key: price-list-2026-10" \
-F "file=@./price list.pdf"import os
import requests
with open("price list.pdf", "rb") as f:
response = requests.post(
"https://wbiztool.com/api/v2/media",
headers={"Authorization": f"Bearer {os.environ['WBIZTOOL_API_KEY']}"},
files={"file": ("price list.pdf", f, "application/pdf")},
timeout=120,
)
media = response.json()
print(response.status_code, media.get("id"), media.get("url"))// Node.js 20+ (built-in fetch, FormData and fs.openAsBlob). Save as .mjs to use top-level await.
import { openAsBlob } from "node:fs";
const form = new FormData();
form.append("file", await openAsBlob("./price list.pdf"), "price list.pdf");
// Don't set Content-Type yourself; fetch adds the multipart boundary.
const response = await fetch("https://wbiztool.com/api/v2/media", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.WBIZTOOL_API_KEY}` },
body: form,
});
const media = await response.json();
console.log(response.status, media.id, media.url);<?php
$ch = curl_init('https://wbiztool.com/api/v2/media');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('WBIZTOOL_API_KEY')],
// An array body makes cURL send multipart/form-data.
CURLOPT_POSTFIELDS => ['file' => new CURLFile('/path/to/price list.pdf', 'application/pdf', 'price list.pdf')],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 120,
]);
$media = json_decode(curl_exec($ch), true);
curl_close($ch);
echo $media['id'] . ' ' . $media['url'];Answer (201):
{
"id": 3301,
"name": "price list.pdf",
"type": "file",
"mime_type": "application/pdf",
"size": 182344,
"url": "https://media.wbiztool.com/media/org_5012/media_5012_17608123450000_0.pdf",
"created_at": "2026-10-18T10:31:40.120Z"
}
Then send it:
{
"to": { "phone": "+919876543210" },
"text": "Here is our new price list.",
"media": { "media_id": 3301 }
}
The recipient sees the name it was uploaded with (price list.pdf); set media.file_name in the message to show another.
Errors when uploading#
| HTTP | Code | When |
|---|---|---|
400 | validation_failed | No file in the field file, or more than one |
413 | file_too_large | The file is over 64 MB |
415 | unsupported_file_type | The extension isn't one of the types above |
415 | unsupported_content_type | The request isn't multipart/form-data (JSON, say) |
422 | storage_full | The file doesn't fit in the plan's storage; used and limit say how much, in bytes |
502 | upload_failed | The file couldn't be stored. Try again (with the same Idempotency-Key) |
List files#
https://wbiztool.com/api/v2/mediaThe workspace's files, newest first, in pages (limit 1 to 100, default 25, and cursor).
curl "https://wbiztool.com/api/v2/media?limit=100" \
-H "Authorization: Bearer $WBIZTOOL_API_KEY"
The answer is {"data": [Media, …], "next_cursor": "…" or null}.
Get a file#
https://wbiztool.com/api/v2/media/{id}curl https://wbiztool.com/api/v2/media/3301 \
-H "Authorization: Bearer $WBIZTOOL_API_KEY"
A file that doesn't exist or was deleted answers 404 media_not_found.
Delete a file#
https://wbiztool.com/api/v2/media/{id}Deletes the file from the Media directory and from storage, so its url stops working. Messages already sent keep their copy on the recipient's phone.
It's refused while something will still send the file: a message that's waiting to go out (including a campaign's), an active reminder, or an active Google Sheets trigger. The answer is then 409 media_in_use, and count says how many. Cancel or change those first, or wait until the messages have gone out.
curl -X DELETE https://wbiztool.com/api/v2/media/3301 \
-H "Authorization: Bearer $WBIZTOOL_API_KEY"
{ "id": 3301, "deleted": true }
| HTTP | Code | When |
|---|---|---|
404 | media_not_found | No such file |
409 | media_in_use | A waiting message, an active reminder or a Google Sheets trigger still uses it |
The Media object#
| Field | Type | Description |
|---|---|---|
id | integer | The file's id: the media_id to send it with. |
name | string | The name it was uploaded with. |
type | string | image for image files, file for everything else. |
mime_type | string | Such as application/pdf or image/jpeg. |
size | integer | Size in bytes. |
url | string | A public link to the file. Anyone with the link can open it, so don't upload files that must stay private. |
created_at | string | When it was uploaded. |
Related#
- Media directory in the dashboard
- Messages → Media: sending with
media_idor a link - Errors
