Messaging

Media uploads and library

Send images, audio, video, documents, and stickers without hosting public URLs. Tyxter stores media by lifecycle and gives providers signed fetch URLs at send time.

Media assets are scoped to one environment and count toward a hard 500 MB active-media quota by default. Use single_use for one-off sends and library for reusable assets. Library assets are retained until deleted.

Choose a lifecycle

Upload library media once, send many times

  1. Create an upload session with POST /v1/media/uploads.
  2. PUT the file bytes directly to the returned storage URL.
  3. Complete the upload with POST /v1/media/uploads/{asset_id}/complete.
  4. Send messages with message.media.asset_id.
GET/v1/media
POST/v1/media/uploads
POST/v1/media/uploads/{'{asset_id}'}/complete
GET/v1/media/storage-usage
GET/v1/media/{'{asset_id}'}
DELETE/v1/media/{'{asset_id}'}

Python broadcast script

This example uploads one image, completes it, then reuses the returnedasset_id across a list of recipients. The script sends individual messages so every recipient can have its own idempotency key.

import mimetypes
import pathlib
import requests

API_BASE = "https://api.tyxter.com"
API_KEY = "tx_live_..."
FROM_PHONE_NUMBER_ID = "pn_..."
RECIPIENTS = ["+5511999999001", "+5511999999002"]
FILE_PATH = pathlib.Path("promo.png")

headers = {
    "authorization": f"Bearer {API_KEY}",
    "content-type": "application/json",
}

data = FILE_PATH.read_bytes()
mime_type = mimetypes.guess_type(FILE_PATH.name)[0] or "application/octet-stream"

upload = requests.post(
    f"{API_BASE}/v1/media/uploads",
    headers={**headers, "idempotency-key": "media-upload-promo-v1"},
    json={
        "kind": "image",
        "lifecycle": "library",
        "filename": FILE_PATH.name,
        "mime_type": mime_type,
        "byte_length": len(data),
    },
    timeout=30,
)
upload.raise_for_status()
session = upload.json()

put = requests.put(
    session["upload_url"],
    headers=session["upload_headers"],
    data=data,
    timeout=120,
)
put.raise_for_status()

complete = requests.post(
    f"{API_BASE}/v1/media/uploads/{session['id']}/complete",
    headers={**headers, "idempotency-key": "media-complete-promo-v1"},
    timeout=30,
)
complete.raise_for_status()
asset_id = complete.json()["id"]

for phone in RECIPIENTS:
    response = requests.post(
        f"{API_BASE}/v1/messages",
        headers={**headers, "idempotency-key": f"promo-image-{phone}"},
        json={
            "channel": "whatsapp",
            "sender": {"type": "whatsapp_phone_number", "id": FROM_PHONE_NUMBER_ID},
            "recipient": {"type": "phone_e164", "id": phone},
            "message": {
                "type": "media",
                "media": {
                    "kind": "image",
                    "asset_id": asset_id,
                    "caption": "Launch promo",
                },
            },
        },
        timeout=30,
    )
    response.raise_for_status()
    print(phone, response.json()["id"])

Inline convenience

For small single sends, message.media.inline can carry base64 bytes directly in the message request. Tyxter immediately stores those bytes as a single_use asset and persists only the generated asset_id. Inline media is limited to 2 MB and should not be used in bulk loops.

Limits