Upload template media
curl --request POST \
--url https://api.omni.z-api.io/whatsapp/businesses/{wabaId}/templates/media \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: multipart/form-data' \
--form file=@example-file \
--form type=image/jpegimport requests
url = "https://api.omni.z-api.io/whatsapp/businesses/{wabaId}/templates/media"
files = { "file": ("example-file", open("example-file", "rb")) }
payload = { "type": "image/jpeg" }
headers = {"Authorization": "Bearer <token>"}
response = requests.post(url, data=payload, files=files, headers=headers)
print(response.text)const form = new FormData();
form.append('file', '<string>');
form.append('type', 'image/jpeg');
const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};
options.body = form;
fetch('https://api.omni.z-api.io/whatsapp/businesses/{wabaId}/templates/media', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.omni.z-api.io/whatsapp/businesses/{wabaId}/templates/media",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\nimage/jpeg\r\n-----011000010111000001101001--",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: multipart/form-data"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.omni.z-api.io/whatsapp/businesses/{wabaId}/templates/media"
payload := strings.NewReader("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\nimage/jpeg\r\n-----011000010111000001101001--")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.omni.z-api.io/whatsapp/businesses/{wabaId}/templates/media")
.header("Authorization", "Bearer <token>")
.body("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\nimage/jpeg\r\n-----011000010111000001101001--")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.omni.z-api.io/whatsapp/businesses/{wabaId}/templates/media")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request.body = "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\nimage/jpeg\r\n-----011000010111000001101001--"
response = http.request(request)
puts response.read_body{
"h": "4::aW1hZ2UvanBlZw==:ARZ...:e:1770000000:..."
}{
"error": "Authorization invalid"
}{
"error": "Content Too Large"
}{
"error": "Unsupported Media Type"
}{
"error": {
"status": 400,
"errors": [
{
"code": "377",
"name": "META_WHATSAPP_MEDIA_URL_NOT_ALLOWED",
"message": "Media URL not allowed"
}
]
}
}{
"error": "<string>"
}{
"error": "integration_timeout",
"message": "The operation took too long to complete. Try again in a few moments."
}Other endpoints
Upload media
Get the header_handle for templates with an image, video or document HEADER
POST
/
whatsapp
/
businesses
/
{wabaId}
/
templates
/
media
Upload template media
curl --request POST \
--url https://api.omni.z-api.io/whatsapp/businesses/{wabaId}/templates/media \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: multipart/form-data' \
--form file=@example-file \
--form type=image/jpegimport requests
url = "https://api.omni.z-api.io/whatsapp/businesses/{wabaId}/templates/media"
files = { "file": ("example-file", open("example-file", "rb")) }
payload = { "type": "image/jpeg" }
headers = {"Authorization": "Bearer <token>"}
response = requests.post(url, data=payload, files=files, headers=headers)
print(response.text)const form = new FormData();
form.append('file', '<string>');
form.append('type', 'image/jpeg');
const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};
options.body = form;
fetch('https://api.omni.z-api.io/whatsapp/businesses/{wabaId}/templates/media', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.omni.z-api.io/whatsapp/businesses/{wabaId}/templates/media",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\nimage/jpeg\r\n-----011000010111000001101001--",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: multipart/form-data"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.omni.z-api.io/whatsapp/businesses/{wabaId}/templates/media"
payload := strings.NewReader("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\nimage/jpeg\r\n-----011000010111000001101001--")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.omni.z-api.io/whatsapp/businesses/{wabaId}/templates/media")
.header("Authorization", "Bearer <token>")
.body("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\nimage/jpeg\r\n-----011000010111000001101001--")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.omni.z-api.io/whatsapp/businesses/{wabaId}/templates/media")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request.body = "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\nimage/jpeg\r\n-----011000010111000001101001--"
response = http.request(request)
puts response.read_body{
"h": "4::aW1hZ2UvanBlZw==:ARZ...:e:1770000000:..."
}{
"error": "Authorization invalid"
}{
"error": "Content Too Large"
}{
"error": "Unsupported Media Type"
}{
"error": {
"status": 400,
"errors": [
{
"code": "377",
"name": "META_WHATSAPP_MEDIA_URL_NOT_ALLOWED",
"message": "Media URL not allowed"
}
]
}
}{
"error": "<string>"
}{
"error": "integration_timeout",
"message": "The operation took too long to complete. Try again in a few moments."
}Concepts
Meta does not accept the file directly when creating the template. It requires aheader_handle produced by the Resumable Upload API, which depends on the application token — that is why the upload goes through here and not through your frontend.
This endpoint takes the file, or its public link, and returns the handle, ready to use.
When you need this
Only when the template has aHEADER component of type IMAGE, VIDEO or DOCUMENT. Templates with a text header, or with no header, need no upload.
Two ways to send
The format is chosen by the requestContent-Type.
- File (multipart)
- Public link (JSON)
multipart/form-data with the file in the file field.The type field is optional. The MIME type used is the one from the file part itself; type only applies when the part arrives without a type or as application/octet-stream.curl -X POST https://api.omni.z-api.io/whatsapp/businesses/{wabaId}/templates/media \
-H "Authorization: Bearer YOUR_SECRET_KEY" \
-F "file=@banner.jpg;type=image/jpeg"
application/json with the public address of the media in the url field.curl -X POST https://api.omni.z-api.io/whatsapp/businesses/{wabaId}/templates/media \
-H "Authorization: Bearer YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://cdn.example.com/banner.jpg"}'
http or https and public: private or internal addresses are rejected.Flow
1
Upload the file or the link
POST /whatsapp/businesses/{wabaId}/templates/media, in one of the two ways above.2
Keep the handle
The response returns the handle in the
h field.3
Use it on the HEADER component
Pass the handle as the HEADER
example in API reference:{
"type": "HEADER",
"format": "IMAGE",
"example": { "header_handle": ["4::aW1hZ2UvanBlZw==:ARZ..."] }
}
The handle expires. Upload the file and create the template in the same session — do not store the handle to use days later.
For larger files, the handle may contain line breaks. Use the value of
h exactly as received, without removing anything.Configuration prerequisite
The upload depends on the Meta App ID registered on the account. Without it the call returns422, with code 375 in the error body.
This is configured in the dashboard, under Security — there is no public endpoint to register the app credentials. See also SDK origins, which covers the other prerequisite of the SDK flow.
Limits
| Item | Value |
|---|---|
| Maximum size per file | 100 MB |
| Maximum size per link | 5 MB |
| Required field (multipart) | file |
| Required field (JSON) | url |
Errors
| Status | When |
|---|---|
400 | Missing or empty file, missing or blank url, invalid JSON, or invalid secret key ({"error": "Authorization invalid"}) |
413 | File larger than 100 MB |
415 | Content-Type other than multipart/form-data and application/json |
422 | The platform or Meta refused the upload, or Meta failed to process it (format, size, expired token or Meta unavailable). The error body comes in error. Most common codes: 375 (channel without App ID), 377 (link not allowed), 380 (failed to download the link) |
502 | Platform unavailable. A Meta refusal or failure does not come as 502: it comes as 422 |
Authorizations
Secret Key generated in the Omni Z-API Security panel
Path Parameters
WABA ID (obtained via List WABAs)
Example:
"428083093730937"
Body
multipart/form-dataapplication/json
Response
Upload completed
Handle to use as the HEADER component example when creating the template