Browse documentation

Messaging

Sending media messages

Send supported image, document, audio, or video content through a connected Sender.

DevelopersAdvancedAvailable

What this guide covers

  • Use a supported media type and a URL the provider can retrieve.
  • Include captions and filenames only where the media type supports them.
  • Track the accepted message asynchronously like other send operations.

Send media from a public URL

V1 accepts a developer-provided mediaUrl. The URL must be a valid HTTP or HTTPS URL without embedded username or password credentials and must remain retrievable while the provider processes the message.

Public media upload is not part of v1. Host the asset using access controls and retention appropriate for its contents.

Send an image
ts
import { WhatsAppClient } from "@yanola/whatsapp"

const apiKey = process.env.YANOLA_API_KEY

if (!apiKey) {
  throw new Error("Missing YANOLA_API_KEY")
}

const yanola = new WhatsAppClient({ apiKey })

const accepted = await yanola.messages.sendMedia({
  from: "+15557654321",
  to: "+15551234567",
  type: "image",
  mediaUrl: "https://cdn.example.com/orders/A123.png",
  caption: "Your order summary",
})

console.log(accepted.id)

Supported media fields

Media message contract
FieldContract
from / toSender and recipient in E.164 format.
typeimage, video, document, or audio.
mediaUrlRequired HTTP or HTTPS URL with no embedded credentials.
captionOptional, up to 1,024 characters; not allowed for audio.
filenameOptional, up to 255 characters; allowed only for documents.

Document and audio examples

Send a document
ts
await yanola.messages.sendMedia({
  from: "+15557654321",
  to: "+15551234567",
  type: "document",
  mediaUrl: "https://cdn.example.com/invoices/A123.pdf",
  caption: "Invoice A123",
  filename: "invoice-A123.pdf",
})
Send audio
ts
await yanola.messages.sendMedia({
  from: "+15557654321",
  to: "+15551234567",
  type: "audio",
  mediaUrl: "https://cdn.example.com/support/reply.ogg",
})

Customer Service Window

In Live Mode, a text, media, location, or contact-card send is allowed only when Yanola has evidence that the recipient sent an inbound message to the same Sender during the previous 24 hours. Activity with another Sender does not open this window.

When no active window is known, Yanola returns 422 service_window_required before queueing the message or making a provider call. The error is not retryable: repeating the same non-template request cannot create a window.

  1. Send an approved template that is appropriate for the business intent.
  2. After the customer replies, use non-template messages during the new 24-hour window.
  3. Do not catch the error and silently substitute unrelated template content.
Customer Service Window error
json
{
  "error": {
    "type": "business_state_error",
    "code": "service_window_required",
    "message": "A text, media, location, or contact-card message requires an active Customer Service Window. Send an approved template instead.",
    "requestId": "req_...",
    "retryable": false,
    "details": {
      "from": "+15557654321",
      "to": "+15551234567"
    }
  }
}

Media delivery considerations

  1. Keep the URL available long enough for asynchronous provider retrieval.
  2. Use the correct Content-Type and a file format supported by WhatsApp for the declared type.
  3. Avoid long-lived public URLs for sensitive files; prefer controlled, expiring access that remains valid through processing.
  4. Use message status and webhook events to detect later provider rejection.

Asynchronous acceptance

Every send method returns a queued response after Yanola accepts the operation. A 202 response is not proof of provider delivery.

Live message IDs start with msg_; Test Mode IDs start with msg_test_. Keep the returned ID and request ID so you can correlate message retrieval, webhook events, logs, failures, and support requests.

Accepted message
json
{
  "id": "msg_...",
  "environment": "live",
  "status": "queued",
  "from": "+15557654321",
  "to": "+15551234567",
  "requestId": "req_...",
  "createdAt": "2026-07-29T10:00:00.000Z"
}

Safe retries and idempotency

The SDK generates a unique Idempotency-Key for every send call and preserves it across automatic transport retries. If your job system owns retry identity, pass a stable idempotencyKey as the second argument.

The same key and request body return the original accepted response. Reusing the key with a different body returns 409 idempotency_key_reused. Yanola retains send idempotency records for at least 24 hours.

Application-owned idempotency
ts
const accepted = await yanola.messages.sendMedia(
  {
    from: "+15557654321",
    to: "+15551234567",
    type: "document",
    mediaUrl: "https://cdn.example.com/invoices/A123.pdf",
    filename: "invoice-A123.pdf",
  },
  {
    idempotencyKey: "invoice-A123-document",
  }
)