Lynkist Developers

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:

PathUse for
POST /media/uploadDirect upload, used by message media (header/body attachments). One HTTP request, multipart form.
POST /media/template-session + POST /media/template-session/uploadResumable upload session for template media. Required by Meta for files in template headers — even small ones must use this protocol.
ScopeEndpoints
media:readGET /media, GET /media/{id}
media:writePOST /media/upload, POST /media/template-session, POST /media/template-session/upload, DELETE /media/{id}

Base path: /api/v1/public/media

Media object

FieldTypeNotes
idUUIDLynkist's internal media ID.
file_namestring | nullOriginal filename you uploaded.
file_typestring | nullMIME type, e.g. image/png.
statusstring | nullUPLOADED, PENDING, FAILED.
public_urlstring | nullOCI Object Storage URL — pass this into image.link / document.link etc. in messages.
created_atISO-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):

MIMEMax size
image/jpeg, image/jpg, image/png5 MB
application/pdf100 MB
video/mp416 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

StatusWhen
400No active WABA for template uploads; invalid MIME; file too large
401Missing / invalid API key
403Key lacks media:read or media:write
404Media ID or upload session not found
500OCI Object Storage upload failed (the media row is created in FAILED status)

Lifecycle notes

  • A direct-upload Media row lands in UPLOADED immediately if OCI succeeds, or FAILED if not. FAILED rows show in GET /media; clean them up manually.
  • Template-session media rows are initially PENDING and flip to UPLOADED after the bytes arrive. FAILED if 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.
Media — Lynkist Developers | Lynkist