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://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://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"
}
]
}The response also has a pagination object. This list uses cursor pagination:
send limit and cursor.
GET /media/{id} — Get media details
curl https://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://lynkist.io/api/v1/public/media/f47ac10b-58cc-4372-a567-0e02b2c3d479 \
-H "Authorization: Bearer $LYNKIST_API_KEY"200 OK — { "id": "<media id>", "deleted": true }. 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://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 |
Any other type returns 415 MEDIA_TYPE_NOT_SUPPORTED. A file over the limit returns
413 PAYLOAD_TOO_LARGE.
200 OK
{
"upload_session_id": "upload:f9bd…"
}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://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
{
"h": "4::aW1hZ2UvanBlZw==:ARZ…",
"media_id": "0b9f6c1e-7d2a-4c55-9a43-2f1d8e6b7a90",
"public_url": "https://…/header_banner.jpg",
"header_handle": "https://…/header_banner.jpg"
}| Field | Meaning |
|---|---|
h | The media handle from Meta. |
media_id | The id of the stored media file in Lynkist. |
public_url | A URL of the stored copy. null if the copy could not be stored. |
header_handle | The value to use as the header example when you call POST /templates. |
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 | error.code | When |
|---|---|---|
400 | VALIDATION_ERROR, INVALID_CURSOR | An invalid field or cursor. |
400, 404, 413, 415 | INVALID_REQUEST, WHATSAPP_NOT_CONNECTED, PAYLOAD_TOO_LARGE, MEDIA_TYPE_NOT_SUPPORTED | The upload is not possible: no active WhatsApp account for a template upload, a file that is too large, or a media type that is not allowed. |
401 | INVALID_API_KEY, API_KEY_EXPIRED | The API key is missing, wrong, revoked or expired. |
403 | INSUFFICIENT_SCOPE | The key lacks media:read or media:write. |
404 | MEDIA_NOT_FOUND | The media id does not exist. |
429 | RATE_LIMIT_EXCEEDED | Rate limit. Wait for Retry-After. |
500 | INTERNAL_ERROR | The storage upload failed (the media row is created in FAILED status). Retry with backoff. |
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.