Media
Upload, list, retrieve and delete media files — plus the resumable session protocol for template media.
The Media API uploads files (images, documents, video, audio) for use in messages and templates. There are two upload paths to pick from:
| Path | Use for |
|---|---|
POST /media/upload | Direct upload, used by message media (header/body attachments). One HTTP request, multipart form. |
POST /media/template-session + POST /media/template-session/upload | Resumable upload session for template media. Required by Meta for files in template headers — even small ones must use this protocol. |
| Scope | Endpoints |
|---|---|
media:read | GET /media, GET /media/{id} |
media:write | POST /media/upload, POST /media/template-session, POST /media/template-session/upload, DELETE /media/{id} |
Base path: /api/v1/public/media
Media object
| Field | Type | Notes |
|---|---|---|
id | UUID | Lynkist's internal media ID. |
file_name | string | null | Original filename you uploaded. |
file_type | string | null | MIME type, e.g. image/png. |
status | string | null | UPLOADED, PENDING, FAILED. |
public_url | string | null | OCI Object Storage URL — pass this into image.link / document.link etc. in messages. |
created_at | ISO-8601 | null |
POST /media/upload — Direct upload
multipart/form-data upload. Use for message media (header images, document attachments on
outgoing messages, etc.). Stores the file in Lynkist's object storage and returns a public URL
you can reference in POST /messages/template components.
curl -X POST https://api.lynkist.io/api/v1/public/media/upload \
-H "Authorization: Bearer $LYNKIST_API_KEY" \
-F "file=@/path/to/banner.jpg;type=image/jpeg"201 Created
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"file_name": "banner.jpg",
"file_type": "image/jpeg",
"status": "UPLOADED",
"public_url": "https://objectstorage.example.com/bucket/path/banner.jpg",
"created_at": "2026-05-31T08:30:00Z"
}Use public_url directly in a send:
{
"components": [
{ "type": "header", "parameters": [
{ "type": "image", "image": { "link": "https://objectstorage.example.com/…/banner.jpg" } }
]}
]
}The direct upload path does not enforce MIME or size limits at the Lynkist edge — Meta will reject anything its own policy disallows at send time. For long-lived template media, prefer the resumable session below, which enforces Meta's limits upfront.
GET /media — List media
curl https://api.lynkist.io/api/v1/public/media \
-H "Authorization: Bearer $LYNKIST_API_KEY"200 OK
{
"data": [
{
"id": "f47ac10b-…",
"file_name": "banner.jpg",
"file_type": "image/jpeg",
"status": "UPLOADED",
"public_url": "https://…",
"created_at": "2026-05-31T08:30:00Z"
}
]
}This endpoint is not paginated — every media object for the tenant is returned. (Paginating it is on the roadmap.)
GET /media/{id} — Get media details
curl https://api.lynkist.io/api/v1/public/media/f47ac10b-58cc-4372-a567-0e02b2c3d479 \
-H "Authorization: Bearer $LYNKIST_API_KEY"200 OK — the media object. 404 if not found.
DELETE /media/{id} — Delete media
Hard-delete: removes the row from Lynkist and (best-effort) the object from storage. Any
template or message that references the public_url directly will break, so make sure nothing
in flight still needs it.
curl -X DELETE https://api.lynkist.io/api/v1/public/media/f47ac10b-58cc-4372-a567-0e02b2c3d479 \
-H "Authorization: Bearer $LYNKIST_API_KEY"200 OK — { "message": "Media deleted" }. 404 if not found.
Resumable upload sessions (template media)
WhatsApp requires template header media (images, PDFs, videos used in template approval) to be uploaded through a Meta-managed resumable upload session. Even files small enough to fit in one HTTP request must use this two-step protocol.
Step 1 — POST /media/template-session — Create the session
curl -X POST https://api.lynkist.io/api/v1/public/media/template-session \
-H "Authorization: Bearer $LYNKIST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"file_name": "header_banner.jpg",
"file_length": 482104,
"file_type": "image/jpeg"
}'Allowed file_type values (enforced by Lynkist):
| MIME | Max size |
|---|---|
image/jpeg, image/jpg, image/png | 5 MB |
application/pdf | 100 MB |
video/mp4 | 16 MB |
Anything else returns 400 Invalid file type for template media.
200 OK
{
"data": { "upload_session_id": "upload:f9bd…" },
"message": "Template media upload session created successfully"
}Step 2 — POST /media/template-session/upload — Upload bytes
multipart/form-data. Send the whole file at once (one chunk) or in chunks by setting
file_offset on each call.
curl -X POST https://api.lynkist.io/api/v1/public/media/template-session/upload \
-H "Authorization: Bearer $LYNKIST_API_KEY" \
-F "media_id=upload:f9bd…" \
-F "file_offset=0" \
-F "file=@/path/to/header_banner.jpg;type=image/jpeg"200 OK — Meta's response payload. The h value in Meta's body is the media handle;
use it in template components when calling POST /templates.
{
"h": "4::aW1hZ2UvanBlZw==:ARZ…"
}Resuming a partial upload
If a chunk fails mid-flight, re-call POST /media/template-session/upload with the same
media_id and the next file_offset. Meta tracks state on its side.
Errors
| Status | When |
|---|---|
400 | No active WABA for template uploads; invalid MIME; file too large |
401 | Missing / invalid API key |
403 | Key lacks media:read or media:write |
404 | Media ID or upload session not found |
500 | OCI Object Storage upload failed (the media row is created in FAILED status) |
Lifecycle notes
- A direct-upload
Mediarow lands inUPLOADEDimmediately if OCI succeeds, orFAILEDif not.FAILEDrows show inGET /media; clean them up manually. - Template-session media rows are initially
PENDINGand flip toUPLOADEDafter the bytes arrive.FAILEDif Meta rejects the chunk. - Media is not deleted automatically when a template is rejected or a message is sent —
call
DELETE /media/{id}when you're done with it.