API documentation

Standard account

API documentation

Send your WhatsApp messages from your own software: approved templates and verification codes, scheduled campaigns and inbox replies.

Base URL

https://podimessage.io/api/v1
Create an API token →

Headers on every request

Authorization: Bearer podi_xxxxxxxxxxxxxxxx
Accept: application/json
Content-Type: application/json

Overview

Your account

Your API token acts as your account: your balance, your connected WhatsApp numbers, your templates, your campaigns and your inbox. You connect your number on the panel’s WhatsApp connection page. Calls use the most recently connected active number; send phone_number_id for another one.

Credits and prices

Amounts in the API (balance, cost, required) are whole numbers in Podi, as the panel shows them: 1 unit of your account currency is 1,000 Podi.

Responses name the unit in currency as PODI.

  • Each template message sent (verification codes and campaign messages included) is charged your account’s message price. The messages/send answer returns it as cost and the balance left as balance.
  • A message WhatsApp refuses when we send it is not charged: the charge is refunded. A message WhatsApp has accepted stays charged, even if it is later reported as failed. Free-text replies inside the 24-hour reply window (inbox replies) are not charged.
  • You top up on the panel’s Balance page; the API does not top up. GET /api/v1/me returns the current balance.

Authentication

Send your API token in the Authorization: Bearer … header of every request. The account owner creates and revokes tokens on the panel’s API & OTP page. Keep tokens on your server, never in a browser or mobile app.

Rate limits

  • Message, template, campaign and inbox endpoints: 60 requests per minute per account.

Responses carry X-RateLimit-Limit and X-RateLimit-Remaining. Over the limit you get 429 with a Retry-After header in seconds.

Formats

  • Send JSON with Content-Type: application/json and Accept: application/json.
  • Times are ISO 8601 with an offset, for example 2026-11-02T10:15:00+03:00. In a query string, encode + as %2B.
  • Phone numbers are digits with the country code, without + or a leading 0: 905321234567.
  • Lists take per_page (1–100, default 50) and page and return meta with current_page, last_page and total. Conversation messages page by id instead.

Errors

Errors are JSON with a message; some add a machine-readable code and other fields. Act on the HTTP status and code, not on the text: messages are written for people and may change. Messages are always in the platform’s default language; Accept-Language does not change it. Send endpoints check in this order: input (422), an active WhatsApp number (409), credit (402).

200 Success.
201 Created.
204 Deleted; no body.
401 Missing or unknown API token.
402 Not enough credit. The body shows balance, and required where it is known.
403 The account is suspended.
404 Unknown id, or one that belongs to another account.
405 Right path, wrong HTTP method; the allowed methods are in the Allow header.
409 No active WhatsApp number, or a request with the same idempotency key is still running.
422 Invalid input, or WhatsApp refused the request (see message and code).
429 Too many requests.
502 WhatsApp did not deliver the media file.

Invalid input returns 422 with the failing fields under errors:

# HTTP 422
{
  "message": "The amount field must be a multiple of 10.",
  "errors": {
    "amount": [
      "The amount field must be a multiple of 10."
    ]
  }
}

Safe retries (idempotency)

Networks fail. On the endpoints below, send a unique key with each operation: repeating the request with the same key within 24 hours returns the first result instead of doing the work twice.

  • messages/send: idempotency_key in the body, per account. Only successful sends are remembered, so a failed send can be retried with the same key. The same key for another recipient or template gets 422 with code: idempotency_key_reused.
  • conversations/{id}/reply: the Idempotency-Key header (or idempotency_key in the body), per conversation. A replayed answer carries Idempotent-Replayed: true; while the first request is still running you get 409 with code: in_progress.
  • otp/send and campaigns take no key: check the outcome before you retry them.

Quick start

From your API token to a two-way conversation in five steps.

  1. 1

    Create an API token

    Create a token on the panel’s API & OTP page. It is shown only once; keep it on your server. Your WhatsApp number must be connected and at least one template approved.

  2. 2

    Check your token

    Returns the account the request runs as and your balance.

    Example request — GET /api/v1/me

    curl 'https://podimessage.io/api/v1/me' \
      -H 'Authorization: Bearer podi_xxxxxxxxxxxxxxxx' \
      -H 'Accept: application/json'
    <?php
    $ch = curl_init('https://podimessage.io/api/v1/me');
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => [
            'Authorization: Bearer podi_xxxxxxxxxxxxxxxx',
            'Accept: application/json',
        ],
    ]);
    $response = json_decode(curl_exec($ch), true);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    const res = await fetch('https://podimessage.io/api/v1/me', {
      headers: {
        Authorization: 'Bearer podi_xxxxxxxxxxxxxxxx',
        Accept: 'application/json',
      },
    });
    const data = await res.json();
  3. 3

    Send an approved template

    Messages you start must use a template Meta has approved (create one on the panel or with POST /api/v1/templates). The answer shows the cost and your new balance.

    Example request — POST /api/v1/messages/send

    curl -X POST 'https://podimessage.io/api/v1/messages/send' \
      -H 'Authorization: Bearer podi_xxxxxxxxxxxxxxxx' \
      -H 'Accept: application/json' \
      -H 'Content-Type: application/json' \
      -d '{
        "to": "905321234567",
        "template": "appointment_reminder",
        "language": "tr",
        "params": [
          "Ayşe",
          "12.11.2026 14:30"
        ],
        "idempotency_key": "reminder-8812"
      }'
    <?php
    $ch = curl_init('https://podimessage.io/api/v1/messages/send');
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => [
            'Authorization: Bearer podi_xxxxxxxxxxxxxxxx',
            'Accept: application/json',
            'Content-Type: application/json',
        ],
        CURLOPT_POSTFIELDS => json_encode([
            'to' => '905321234567',
            'template' => 'appointment_reminder',
            'language' => 'tr',
            'params' => ['Ayşe', '12.11.2026 14:30'],
            'idempotency_key' => 'reminder-8812',
        ]),
    ]);
    $response = json_decode(curl_exec($ch), true);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    const res = await fetch('https://podimessage.io/api/v1/messages/send', {
      method: 'POST',
      headers: {
        Authorization: 'Bearer podi_xxxxxxxxxxxxxxxx',
        Accept: 'application/json',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        to: '905321234567',
        template: 'appointment_reminder',
        language: 'tr',
        params: ['Ayşe', '12.11.2026 14:30'],
        idempotency_key: 'reminder-8812',
      }),
    });
    const data = await res.json();
  4. 4

    Get the replies

    When the customer writes back, the conversation is updated. Poll GET /api/v1/conversations with updated_since and fetch a conversation’s new messages with after_id.

    Example request — GET /api/v1/conversations

    curl 'https://podimessage.io/api/v1/conversations?status=pool&updated_since=2026-11-02T10%3A00%3A00%2B03%3A00&per_page=20' \
      -H 'Authorization: Bearer podi_xxxxxxxxxxxxxxxx' \
      -H 'Accept: application/json'
    <?php
    $ch = curl_init('https://podimessage.io/api/v1/conversations?status=pool&updated_since=2026-11-02T10%3A00%3A00%2B03%3A00&per_page=20');
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => [
            'Authorization: Bearer podi_xxxxxxxxxxxxxxxx',
            'Accept: application/json',
        ],
    ]);
    $response = json_decode(curl_exec($ch), true);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    const res = await fetch('https://podimessage.io/api/v1/conversations?status=pool&updated_since=2026-11-02T10%3A00%3A00%2B03%3A00&per_page=20', {
      headers: {
        Authorization: 'Bearer podi_xxxxxxxxxxxxxxxx',
        Accept: 'application/json',
      },
    });
    const data = await res.json();
  5. 5

    Answer it

    For 24 hours after the customer’s last message you can reply with free text. These replies are not charged to the balance; Meta charges its own fee after the first 1,000 service messages a month on each number.

    Example request — POST /api/v1/conversations/{conversation}/reply

    curl -X POST 'https://podimessage.io/api/v1/conversations/12/reply' \
      -H 'Authorization: Bearer podi_xxxxxxxxxxxxxxxx' \
      -H 'Idempotency-Key: reply-77f1c2' \
      -H 'Accept: application/json' \
      -H 'Content-Type: application/json' \
      -d '{
        "text": "Merhaba Ayşe, yarın 14:30 için randevunuzu oluşturduk."
      }'
    <?php
    $ch = curl_init('https://podimessage.io/api/v1/conversations/12/reply');
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => [
            'Authorization: Bearer podi_xxxxxxxxxxxxxxxx',
            'Idempotency-Key: reply-77f1c2',
            'Accept: application/json',
            'Content-Type: application/json',
        ],
        CURLOPT_POSTFIELDS => json_encode([
            'text' => 'Merhaba Ayşe, yarın 14:30 için randevunuzu oluşturduk.',
        ]),
    ]);
    $response = json_decode(curl_exec($ch), true);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    const res = await fetch('https://podimessage.io/api/v1/conversations/12/reply', {
      method: 'POST',
      headers: {
        Authorization: 'Bearer podi_xxxxxxxxxxxxxxxx',
        'Idempotency-Key': 'reply-77f1c2',
        Accept: 'application/json',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        text: 'Merhaba Ayşe, yarın 14:30 için randevunuzu oluşturduk.',
      }),
    });
    const data = await res.json();

Messages and templates

Send templates and verification codes, manage templates and run campaigns. Amounts come back as PODI.

Campaign object
id integer
Campaign id.
name string
Campaign name.
template string
Template name.
language string
Template language.
category string
Billing category: marketing, utility or authentication.
status string
scheduled, queued, processing, completed, failed or cancelled.
scheduled_at string|null
Planned start, or null when sent at once.
total_recipients integer
Number of recipients.
sent integer
Messages WhatsApp accepted.
failed integer
Messages that could not be sent.
delivered integer
Messages delivered to the phone.
read integer
Messages read.
started_at string|null
When sending started.
completed_at string|null
When every recipient had been processed.
GET /api/v1/me Check your token: returns the account the request runs as and its balance.
API token

Example request

curl 'https://podimessage.io/api/v1/me' \
  -H 'Authorization: Bearer podi_xxxxxxxxxxxxxxxx' \
  -H 'Accept: application/json'
<?php
$ch = curl_init('https://podimessage.io/api/v1/me');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer podi_xxxxxxxxxxxxxxxx',
        'Accept: application/json',
    ],
]);
$response = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
const res = await fetch('https://podimessage.io/api/v1/me', {
  headers: {
    Authorization: 'Bearer podi_xxxxxxxxxxxxxxxx',
    Accept: 'application/json',
  },
});
const data = await res.json();

Response · HTTP 200

{
  "id": 57,
  "name": "Pati Klinik",
  "email": "info@example.com",
  "balance": 50000,
  "currency": "PODI"
}
POST /api/v1/messages/send Send one approved template to one number. It is sent at once and charged to your balance.
API token

To answer a customer who wrote in the last 24 hours, use the inbox reply instead: it is not charged to the balance.

Parameters

to
string · body required
Recipient number: digits with the country code, up to 20 characters.
template
string · body required
Name of an approved template on your account.
language
string · body
Template language code, such as tr or en_US. The default is en_US, so set it for Turkish templates.
params
array · body
Up to 10 strings (1,000 characters each) that fill {{1}}, {{2}}… in the template body, in order.
phone_number_id
string · body
Which of your connected numbers to use. Default: the most recently connected active number.
idempotency_key
string · body
Optional unique key, up to 128 characters. Repeating it within 24 hours returns the first successful result and sends nothing.

Example request

curl -X POST 'https://podimessage.io/api/v1/messages/send' \
  -H 'Authorization: Bearer podi_xxxxxxxxxxxxxxxx' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
    "to": "905321234567",
    "template": "appointment_reminder",
    "language": "tr",
    "params": [
      "Ayşe",
      "12.11.2026 14:30"
    ],
    "idempotency_key": "reminder-8812"
  }'
<?php
$ch = curl_init('https://podimessage.io/api/v1/messages/send');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer podi_xxxxxxxxxxxxxxxx',
        'Accept: application/json',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'to' => '905321234567',
        'template' => 'appointment_reminder',
        'language' => 'tr',
        'params' => ['Ayşe', '12.11.2026 14:30'],
        'idempotency_key' => 'reminder-8812',
    ]),
]);
$response = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
const res = await fetch('https://podimessage.io/api/v1/messages/send', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer podi_xxxxxxxxxxxxxxxx',
    Accept: 'application/json',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    to: '905321234567',
    template: 'appointment_reminder',
    language: 'tr',
    params: ['Ayşe', '12.11.2026 14:30'],
    idempotency_key: 'reminder-8812',
  }),
});
const data = await res.json();

Response · HTTP 200

{
  "status": "sent",
  "message_id": 1234,
  "wamid": "wamid.HBgMOTA1MzIxMjM0NTY3FQIAERgSQ0Y2…",
  "to": "905321234567",
  "cost": 20,
  "currency": "PODI",
  "balance": 49980
}

Other responses

402 Your balance cannot pay for the message; nothing was sent.
{
  "message": "Insufficient balance. Top up and retry the request. The balance field in the response shows the current balance, and required (if present) the amount needed.",
  "required": 20,
  "balance": 0,
  "currency": "PODI"
}
409 Your account has no active WhatsApp number (or not the phone_number_id you named).
{
  "message": "No active WhatsApp number is connected. Connect one in the panel."
}
422 WhatsApp refused the message: code is Meta’s error code and message_id the failed record. The charge is refunded.
{
  "message": "(#132001) Template name does not exist in the translation",
  "code": "132001",
  "message_id": 1234
}
POST /api/v1/otp/send Send a verification code with an approved Authentication template. You generate and check the code yourself.
API token

Parameters

to
string · body required
Recipient number: digits with the country code, up to 20 characters.
template
string · body required
Name of an approved template in the Authentication category.
code
string · body required
The code you generated, up to 30 characters. It fills the template body and its button.
language
string · body
Template language code, such as tr or en_US. The default is en_US, so set it for Turkish templates.
include_button
boolean · body
Default true. Set it to false if the template has no button.
button_sub_type
string · body
url (default; works for one-tap and copy-code buttons) or copy_code.
phone_number_id
string · body
Which of your connected numbers to use. Default: the most recently connected active number.

Example request

curl -X POST 'https://podimessage.io/api/v1/otp/send' \
  -H 'Authorization: Bearer podi_xxxxxxxxxxxxxxxx' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
    "to": "905321234567",
    "template": "login_code",
    "language": "tr",
    "code": "482913"
  }'
<?php
$ch = curl_init('https://podimessage.io/api/v1/otp/send');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer podi_xxxxxxxxxxxxxxxx',
        'Accept: application/json',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'to' => '905321234567',
        'template' => 'login_code',
        'language' => 'tr',
        'code' => '482913',
    ]),
]);
$response = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
const res = await fetch('https://podimessage.io/api/v1/otp/send', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer podi_xxxxxxxxxxxxxxxx',
    Accept: 'application/json',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    to: '905321234567',
    template: 'login_code',
    language: 'tr',
    code: '482913',
  }),
});
const data = await res.json();

Response · HTTP 200

{
  "status": "sent",
  "message_id": 1234,
  "wamid": "wamid.HBgMOTA1MzIxMjM0NTY3FQIAERgSQ0Y2…",
  "to": "905321234567",
  "cost": 20,
  "currency": "PODI",
  "balance": 49980
}

Other responses

402 Your balance cannot pay for the message; nothing was sent.
409 Your account has no active WhatsApp number (or not the phone_number_id you named).
{
  "message": "No active WhatsApp number is connected. Connect one in the panel."
}
422 WhatsApp refused the message: code is Meta’s error code and message_id the failed record. The charge is refunded.
GET /api/v1/templates Your account’s templates as Meta returns them (cached for up to 5 minutes).
API token

Parameters

status
string · query
ALL (default), APPROVED, PENDING, REJECTED, PAUSED or DISABLED.
phone_number_id
string · query
Which of your connected numbers to use. Default: the most recently connected active number.

Example request

curl 'https://podimessage.io/api/v1/templates?status=APPROVED' \
  -H 'Authorization: Bearer podi_xxxxxxxxxxxxxxxx' \
  -H 'Accept: application/json'
<?php
$ch = curl_init('https://podimessage.io/api/v1/templates?status=APPROVED');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer podi_xxxxxxxxxxxxxxxx',
        'Accept: application/json',
    ],
]);
$response = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
const res = await fetch('https://podimessage.io/api/v1/templates?status=APPROVED', {
  headers: {
    Authorization: 'Bearer podi_xxxxxxxxxxxxxxxx',
    Accept: 'application/json',
  },
});
const data = await res.json();

Response · HTTP 200

{
  "data": [
    {
      "id": "1489022265523517",
      "name": "appointment_reminder",
      "language": "tr",
      "category": "UTILITY",
      "components": [
        {
          "type": "BODY",
          "text": "Merhaba {{1}}, {{2}} tarihli randevunuzu hatırlatırız.",
          "example": {
            "body_text": [
              [
                "Ayşe",
                "12.11.2026 14:30"
              ]
            ]
          }
        }
      ],
      "status": "APPROVED",
      "rejected_reason": "NONE"
    }
  ]
}

Other responses

409 Your account has no active WhatsApp number (or not the phone_number_id you named).
422 WhatsApp refused the request; code is Meta’s error code.
POST /api/v1/templates Submit a new template for Meta’s review. It starts as PENDING; follow the outcome with GET /api/v1/templates.
API token

Parameters

name
string · body required
Lowercase letters, digits and _, up to 512 characters.
language
string · body required
Language code, such as tr.
category
string · body required
MARKETING, UTILITY or AUTHENTICATION.
components
array · body required
Up to 10 components in Meta’s template format (header, body, footer, buttons). Body variables need an example.
phone_number_id
string · body
Which of your connected numbers to use. Default: the most recently connected active number.

Example request

curl -X POST 'https://podimessage.io/api/v1/templates' \
  -H 'Authorization: Bearer podi_xxxxxxxxxxxxxxxx' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "appointment_reminder",
    "language": "tr",
    "category": "UTILITY",
    "components": [
      {
        "type": "BODY",
        "text": "Merhaba {{1}}, {{2}} tarihli randevunuzu hatırlatırız.",
        "example": {
          "body_text": [
            [
              "Ayşe",
              "12.11.2026 14:30"
            ]
          ]
        }
      }
    ]
  }'
<?php
$ch = curl_init('https://podimessage.io/api/v1/templates');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer podi_xxxxxxxxxxxxxxxx',
        'Accept: application/json',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'name' => 'appointment_reminder',
        'language' => 'tr',
        'category' => 'UTILITY',
        'components' => [
            [
                'type' => 'BODY',
                'text' => 'Merhaba {{1}}, {{2}} tarihli randevunuzu hatırlatırız.',
                'example' => [
                    'body_text' => [
                        ['Ayşe', '12.11.2026 14:30'],
                    ],
                ],
            ],
        ],
    ]),
]);
$response = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
const res = await fetch('https://podimessage.io/api/v1/templates', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer podi_xxxxxxxxxxxxxxxx',
    Accept: 'application/json',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 'appointment_reminder',
    language: 'tr',
    category: 'UTILITY',
    components: [
      {
        type: 'BODY',
        text: 'Merhaba {{1}}, {{2}} tarihli randevunuzu hatırlatırız.',
        example: {
          body_text: [
            ['Ayşe', '12.11.2026 14:30'],
          ],
        },
      },
    ],
  }),
});
const data = await res.json();

Response · HTTP 201

{
  "id": "1489022265523517",
  "status": "PENDING",
  "category": "UTILITY"
}

Other responses

409 Your account has no active WhatsApp number (or not the phone_number_id you named).
422 WhatsApp refused the request; code is Meta’s error code.
DELETE /api/v1/templates/{name} Delete a template on Meta.
API token

Parameters

name
string · path required
Template name. Every language of that name is deleted.
phone_number_id
string · query
Which of your connected numbers to use. Default: the most recently connected active number.

Example request

curl -X DELETE 'https://podimessage.io/api/v1/templates/appointment_reminder' \
  -H 'Authorization: Bearer podi_xxxxxxxxxxxxxxxx' \
  -H 'Accept: application/json'
<?php
$ch = curl_init('https://podimessage.io/api/v1/templates/appointment_reminder');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'DELETE',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer podi_xxxxxxxxxxxxxxxx',
        'Accept: application/json',
    ],
]);
$response = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
const res = await fetch('https://podimessage.io/api/v1/templates/appointment_reminder', {
  method: 'DELETE',
  headers: {
    Authorization: 'Bearer podi_xxxxxxxxxxxxxxxx',
    Accept: 'application/json',
  },
});
const data = await res.json();

Response · HTTP 200

{
  "status": "deleted",
  "name": "appointment_reminder"
}

Other responses

409 Your account has no active WhatsApp number (or not the phone_number_id you named).
422 WhatsApp refused the request; code is Meta’s error code.
POST /api/v1/campaigns Send one approved template to up to 5,000 recipients, now or at scheduled_at. The balance is checked up front, and each message is charged when it is sent.
API token

A campaign sent at once goes queued → processing → completed; a scheduled one goes scheduled → processing → completed, or failed if the balance no longer covers it at the planned time. Cancel a scheduled one before it starts with DELETE /api/v1/campaigns/{campaign}, and follow it with GET /api/v1/campaigns/{campaign}.

Parameters

name
string · body required
Your name for the campaign, up to 120 characters.
template
string · body required
Name of an approved template.
language
string · body
Template language. Without it, the first template with that name is used.
recipients
array · body required
1 to 5,000 recipients.
recipients.*.phone
string · body required
Number with the country code, up to 20 characters; spaces and symbols are removed.
recipients.*.name
string · body
Optional, up to 120 characters.
recipients.*.params
array · body
Up to 10 strings for {{1}}, {{2}}… for this recipient.
scheduled_at
date · body
Optional time in the future (ISO 8601). Without it, sending starts at once.
phone_number_id
string · body
Which of your connected numbers to use. Default: the most recently connected active number.

Example request

curl -X POST 'https://podimessage.io/api/v1/campaigns' \
  -H 'Authorization: Bearer podi_xxxxxxxxxxxxxxxx' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Kasım aşı hatırlatmaları",
    "template": "appointment_reminder",
    "language": "tr",
    "recipients": [
      {
        "phone": "905321234567",
        "name": "Ayşe",
        "params": [
          "Ayşe",
          "12.11.2026 14:30"
        ]
      },
      {
        "phone": "905339876543",
        "name": "Mehmet",
        "params": [
          "Mehmet",
          "13.11.2026 10:00"
        ]
      }
    ],
    "scheduled_at": "2026-11-10T09:00:00+03:00"
  }'
<?php
$ch = curl_init('https://podimessage.io/api/v1/campaigns');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer podi_xxxxxxxxxxxxxxxx',
        'Accept: application/json',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'name' => 'Kasım aşı hatırlatmaları',
        'template' => 'appointment_reminder',
        'language' => 'tr',
        'recipients' => [
            [
                'phone' => '905321234567',
                'name' => 'Ayşe',
                'params' => ['Ayşe', '12.11.2026 14:30'],
            ],
            [
                'phone' => '905339876543',
                'name' => 'Mehmet',
                'params' => ['Mehmet', '13.11.2026 10:00'],
            ],
        ],
        'scheduled_at' => '2026-11-10T09:00:00+03:00',
    ]),
]);
$response = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
const res = await fetch('https://podimessage.io/api/v1/campaigns', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer podi_xxxxxxxxxxxxxxxx',
    Accept: 'application/json',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 'Kasım aşı hatırlatmaları',
    template: 'appointment_reminder',
    language: 'tr',
    recipients: [
      {
        phone: '905321234567',
        name: 'Ayşe',
        params: ['Ayşe', '12.11.2026 14:30'],
      },
      {
        phone: '905339876543',
        name: 'Mehmet',
        params: ['Mehmet', '13.11.2026 10:00'],
      },
    ],
    scheduled_at: '2026-11-10T09:00:00+03:00',
  }),
});
const data = await res.json();

Response · HTTP 201

{
  "id": 7,
  "name": "Kasım aşı hatırlatmaları",
  "template": "appointment_reminder",
  "language": "tr",
  "category": "utility",
  "status": "scheduled",
  "scheduled_at": "2026-11-10T09:00:00+03:00",
  "total_recipients": 2,
  "sent": 0,
  "failed": 0,
  "delivered": 0,
  "read": 0,
  "started_at": null,
  "completed_at": null
}

Other responses

402 Your balance cannot pay for every recipient.
{
  "message": "Insufficient balance. Top up and retry the request. The balance field in the response shows the current balance, and required (if present) the amount needed.",
  "required": 40,
  "balance": 20,
  "currency": "PODI"
}
409 Your account has no active WhatsApp number (or not the phone_number_id you named).
422 No approved template with that name (and language).
{
  "message": "No approved template with this name (and language, if given). Check the template and language values."
}
GET /api/v1/campaigns/{campaign} A campaign and its progress.
API token

Parameters

campaign
integer · path required
The campaign’s id.

Example request

curl 'https://podimessage.io/api/v1/campaigns/7' \
  -H 'Authorization: Bearer podi_xxxxxxxxxxxxxxxx' \
  -H 'Accept: application/json'
<?php
$ch = curl_init('https://podimessage.io/api/v1/campaigns/7');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer podi_xxxxxxxxxxxxxxxx',
        'Accept: application/json',
    ],
]);
$response = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
const res = await fetch('https://podimessage.io/api/v1/campaigns/7', {
  headers: {
    Authorization: 'Bearer podi_xxxxxxxxxxxxxxxx',
    Accept: 'application/json',
  },
});
const data = await res.json();

Response · HTTP 200

{
  "id": 7,
  "name": "Kasım aşı hatırlatmaları",
  "template": "appointment_reminder",
  "language": "tr",
  "category": "utility",
  "status": "scheduled",
  "scheduled_at": "2026-11-10T09:00:00+03:00",
  "total_recipients": 2,
  "sent": 0,
  "failed": 0,
  "delivered": 0,
  "read": 0,
  "started_at": null,
  "completed_at": null
}

Other responses

404 No such campaign on this account.
{
  "message": "Campaign not found."
}
GET /api/v1/campaigns Your campaigns (from the panel and the API), newest first.
API token

Parameters

status
string · query
Only campaigns with this status: scheduled, queued, processing, completed, failed or cancelled.
per_page
integer · query
1–100, default 50.
page
integer · query
Page number, default 1.

Example request

curl 'https://podimessage.io/api/v1/campaigns?status=scheduled&per_page=20' \
  -H 'Authorization: Bearer podi_xxxxxxxxxxxxxxxx' \
  -H 'Accept: application/json'
<?php
$ch = curl_init('https://podimessage.io/api/v1/campaigns?status=scheduled&per_page=20');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer podi_xxxxxxxxxxxxxxxx',
        'Accept: application/json',
    ],
]);
$response = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
const res = await fetch('https://podimessage.io/api/v1/campaigns?status=scheduled&per_page=20', {
  headers: {
    Authorization: 'Bearer podi_xxxxxxxxxxxxxxxx',
    Accept: 'application/json',
  },
});
const data = await res.json();

Response · HTTP 200

{
  "data": [
    {
      "id": 7,
      "name": "Kasım aşı hatırlatmaları",
      "template": "appointment_reminder",
      "language": "tr",
      "category": "utility",
      "status": "scheduled",
      "scheduled_at": "2026-11-10T09:00:00+03:00",
      "total_recipients": 2,
      "sent": 0,
      "failed": 0,
      "delivered": 0,
      "read": 0,
      "started_at": null,
      "completed_at": null
    }
  ],
  "meta": {
    "current_page": 1,
    "last_page": 1,
    "total": 1
  }
}
DELETE /api/v1/campaigns/{campaign} Cancel a scheduled campaign before it starts: nothing is sent and nothing is charged.
API token

Only a scheduled campaign can be cancelled. At its planned time it becomes processing and can no longer be stopped, so cancel before then. Once a cancel succeeds, nothing is sent.

Parameters

campaign
integer · path required
The campaign’s id.

Example request

curl -X DELETE 'https://podimessage.io/api/v1/campaigns/7' \
  -H 'Authorization: Bearer podi_xxxxxxxxxxxxxxxx' \
  -H 'Accept: application/json'
<?php
$ch = curl_init('https://podimessage.io/api/v1/campaigns/7');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'DELETE',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer podi_xxxxxxxxxxxxxxxx',
        'Accept: application/json',
    ],
]);
$response = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
const res = await fetch('https://podimessage.io/api/v1/campaigns/7', {
  method: 'DELETE',
  headers: {
    Authorization: 'Bearer podi_xxxxxxxxxxxxxxxx',
    Accept: 'application/json',
  },
});
const data = await res.json();

Response · HTTP 200

{
  "id": 7,
  "name": "Kasım aşı hatırlatmaları",
  "template": "appointment_reminder",
  "language": "tr",
  "category": "utility",
  "status": "cancelled",
  "scheduled_at": "2026-11-10T09:00:00+03:00",
  "total_recipients": 2,
  "sent": 0,
  "failed": 0,
  "delivered": 0,
  "read": 0,
  "started_at": null,
  "completed_at": null
}

Other responses

404 No such campaign on this account.
409 The campaign is no longer scheduled (it started, finished or was already cancelled); status shows where it is. A started campaign cannot be stopped.
{
  "message": "The campaign is no longer scheduled: it has started, finished or been cancelled, so it cannot be cancelled.",
  "code": "not_scheduled",
  "status": "processing"
}

Inbox

Read your WhatsApp conversations and answer them from your software.

The 24-hour reply window

WhatsApp lets a business send free text only within 24 hours of the customer’s last message. can_reply and reply_window_expires_at tell you whether the window is open. Once it has closed, reply returns 422 with code: window_closed: send an approved template with POST /api/v1/messages/send instead (charged like any template). The window opens again when the customer answers.

Staying in sync

For new messages, poll GET /api/v1/conversations with updated_since and fetch a conversation’s new messages with after_id.

An API reply does not take the chat over from the panel: its status and assigned team member stay as they are, but the customer no longer counts as waiting for a reply. POST …/read clears the unread badge for panel users too.

Conversation object
id integer
Conversation id.
wa_id string
The customer’s WhatsApp number (digits).
name string|null
The customer’s WhatsApp profile name.
phone_number_id string|null
Your number this chat is on. Pass it to messages/send to write from the same number.
status string
pool (in Waiting, not yet taken by anyone), assigned or closed, as in the panel.
unread_count integer
Customer messages not yet read.
last_message_preview string|null
Short text of the last message.
last_message_at string|null
Time of the last message in either direction.
last_inbound_at string|null
Time of the customer’s last message.
can_reply boolean
true while the 24-hour reply window is open and the number is active.
reply_window_expires_at string|null
When the reply window closes (in the past once closed); null if the customer has never written.
updated_at string|null
Last change; use it with updated_since.
Message object
id integer
Message id.
direction string
inbound (from the customer) or outbound.
type string
text, image, video, audio, document, sticker, location, button, interactive…
text string|null
Text or media caption.
media object|null
null, or type, mime, filename (documents) and download_path, which is relative to the API host and needs the same headers.
status string|null
sent, delivered, read or failed for outbound messages; null for inbound ones.
source string|null
Where an outbound message came from: panel, api, auto_reply, template or business_app (sent from the WhatsApp Business app on the business’s phone); null for inbound ones.
created_at string
When the message was stored.
GET /api/v1/conversations Your conversations, most recent first.
API token

Parameters

status
string · query
pool (in Waiting, not yet taken by anyone), assigned, closed or all (default).
updated_since
date · query
Only conversations changed at or after this time (ISO 8601). New messages, replies and reads all count as changes.
wa_id
string · query
Only the conversation with this customer number; anything but digits is ignored.
per_page
integer · query
1–100, default 50.
page
integer · query
Page number, default 1.

Example request

curl 'https://podimessage.io/api/v1/conversations?status=pool&updated_since=2026-11-02T10%3A00%3A00%2B03%3A00&per_page=20' \
  -H 'Authorization: Bearer podi_xxxxxxxxxxxxxxxx' \
  -H 'Accept: application/json'
<?php
$ch = curl_init('https://podimessage.io/api/v1/conversations?status=pool&updated_since=2026-11-02T10%3A00%3A00%2B03%3A00&per_page=20');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer podi_xxxxxxxxxxxxxxxx',
        'Accept: application/json',
    ],
]);
$response = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
const res = await fetch('https://podimessage.io/api/v1/conversations?status=pool&updated_since=2026-11-02T10%3A00%3A00%2B03%3A00&per_page=20', {
  headers: {
    Authorization: 'Bearer podi_xxxxxxxxxxxxxxxx',
    Accept: 'application/json',
  },
});
const data = await res.json();

Response · HTTP 200

{
  "data": [
    {
      "id": 12,
      "wa_id": "905321234567",
      "name": "Ayşe Yılmaz",
      "phone_number_id": "106540352242922",
      "status": "pool",
      "unread_count": 1,
      "last_message_preview": "Yarın 14:30 uygun mu?",
      "last_message_at": "2026-11-02T10:12:00+03:00",
      "last_inbound_at": "2026-11-02T10:12:00+03:00",
      "can_reply": true,
      "reply_window_expires_at": "2026-11-03T10:12:00+03:00",
      "updated_at": "2026-11-02T10:12:00+03:00"
    }
  ],
  "meta": {
    "current_page": 1,
    "last_page": 1,
    "per_page": 20,
    "total": 1
  }
}
GET /api/v1/conversations/{conversation} One conversation.
API token

Parameters

conversation
integer · path required
The conversation’s id.

Example request

curl 'https://podimessage.io/api/v1/conversations/12' \
  -H 'Authorization: Bearer podi_xxxxxxxxxxxxxxxx' \
  -H 'Accept: application/json'
<?php
$ch = curl_init('https://podimessage.io/api/v1/conversations/12');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer podi_xxxxxxxxxxxxxxxx',
        'Accept: application/json',
    ],
]);
$response = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
const res = await fetch('https://podimessage.io/api/v1/conversations/12', {
  headers: {
    Authorization: 'Bearer podi_xxxxxxxxxxxxxxxx',
    Accept: 'application/json',
  },
});
const data = await res.json();

Response · HTTP 200

{
  "id": 12,
  "wa_id": "905321234567",
  "name": "Ayşe Yılmaz",
  "phone_number_id": "106540352242922",
  "status": "pool",
  "unread_count": 1,
  "last_message_preview": "Yarın 14:30 uygun mu?",
  "last_message_at": "2026-11-02T10:12:00+03:00",
  "last_inbound_at": "2026-11-02T10:12:00+03:00",
  "can_reply": true,
  "reply_window_expires_at": "2026-11-03T10:12:00+03:00",
  "updated_at": "2026-11-02T10:12:00+03:00"
}

Other responses

404 No such conversation on this account.
{
  "message": "Conversation not found."
}
GET /api/v1/conversations/{conversation}/messages A conversation’s messages, newest first.
API token

To page back, pass meta.next_before_id as before_id while has_more is true.

Parameters

conversation
integer · path required
The conversation’s id.
per_page
integer · query
1–100, default 50.
before_id
integer · query
Only messages older than this id.
after_id
integer · query
Only messages newer than this id, for polling. Keep it when paging back.

Example request

curl 'https://podimessage.io/api/v1/conversations/12/messages?per_page=2' \
  -H 'Authorization: Bearer podi_xxxxxxxxxxxxxxxx' \
  -H 'Accept: application/json'
<?php
$ch = curl_init('https://podimessage.io/api/v1/conversations/12/messages?per_page=2');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer podi_xxxxxxxxxxxxxxxx',
        'Accept: application/json',
    ],
]);
$response = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
const res = await fetch('https://podimessage.io/api/v1/conversations/12/messages?per_page=2', {
  headers: {
    Authorization: 'Bearer podi_xxxxxxxxxxxxxxxx',
    Accept: 'application/json',
  },
});
const data = await res.json();

Response · HTTP 200

{
  "data": [
    {
      "id": 345,
      "direction": "inbound",
      "type": "text",
      "text": "Yarın 14:30 uygun mu?",
      "media": null,
      "status": null,
      "source": null,
      "created_at": "2026-11-02T10:12:00+03:00"
    },
    {
      "id": 344,
      "direction": "inbound",
      "type": "image",
      "text": "Aşı karnesi",
      "media": {
        "type": "image",
        "mime": "image/jpeg",
        "filename": null,
        "download_path": "/api/v1/messages/344/media"
      },
      "status": null,
      "source": null,
      "created_at": "2026-11-02T10:11:00+03:00"
    }
  ],
  "meta": {
    "per_page": 2,
    "has_more": true,
    "next_before_id": 344
  }
}

Other responses

404 No such conversation on this account.
{
  "message": "Conversation not found."
}
POST /api/v1/conversations/{conversation}/reply Answer with free text inside the 24-hour reply window. Not charged.
API token

Parameters

conversation
integer · path required
The conversation’s id.
text
string · body required
Message text, 1–4,096 characters.
Idempotency-Key
string · header
Optional unique key, up to 128 characters (or idempotency_key in the body). A repeat within 24 hours returns the first answer and sends nothing.

Example request

curl -X POST 'https://podimessage.io/api/v1/conversations/12/reply' \
  -H 'Authorization: Bearer podi_xxxxxxxxxxxxxxxx' \
  -H 'Idempotency-Key: reply-77f1c2' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
    "text": "Merhaba Ayşe, yarın 14:30 için randevunuzu oluşturduk."
  }'
<?php
$ch = curl_init('https://podimessage.io/api/v1/conversations/12/reply');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer podi_xxxxxxxxxxxxxxxx',
        'Idempotency-Key: reply-77f1c2',
        'Accept: application/json',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'text' => 'Merhaba Ayşe, yarın 14:30 için randevunuzu oluşturduk.',
    ]),
]);
$response = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
const res = await fetch('https://podimessage.io/api/v1/conversations/12/reply', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer podi_xxxxxxxxxxxxxxxx',
    'Idempotency-Key': 'reply-77f1c2',
    Accept: 'application/json',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    text: 'Merhaba Ayşe, yarın 14:30 için randevunuzu oluşturduk.',
  }),
});
const data = await res.json();

Response · HTTP 200

{
  "status": "sent",
  "message": {
    "id": 346,
    "direction": "outbound",
    "type": "text",
    "text": "Merhaba Ayşe, yarın 14:30 için randevunuzu oluşturduk.",
    "media": null,
    "status": "sent",
    "source": "api",
    "created_at": "2026-11-02T10:15:00+03:00"
  },
  "conversation": {
    "id": 12,
    "wa_id": "905321234567",
    "name": "Ayşe Yılmaz",
    "phone_number_id": "106540352242922",
    "status": "pool",
    "unread_count": 1,
    "last_message_preview": "Yarın 14:30 uygun mu?",
    "last_message_at": "2026-11-02T10:12:00+03:00",
    "last_inbound_at": "2026-11-02T10:12:00+03:00",
    "can_reply": true,
    "reply_window_expires_at": "2026-11-03T10:12:00+03:00",
    "updated_at": "2026-11-02T10:12:00+03:00"
  }
}

Other responses

422 The 24-hour reply window has closed (also when WhatsApp reports it); send a template instead.
{
  "message": "The 24-hour reply window is closed because the customer has not written in the last 24 hours. Send an approved template with POST /api/v1/messages/send instead.",
  "code": "window_closed",
  "template_endpoint": "/api/v1/messages/send"
}
409 The conversation’s number is no longer active.
{
  "message": "No active WhatsApp number is connected. Connect one in the panel.",
  "code": "no_account"
}
409 A request with the same Idempotency-Key is still running.
{
  "message": "A request with this idempotency key is still being processed. Try again shortly.",
  "code": "in_progress"
}
422 WhatsApp refused the reply; retryable says whether trying again may help. Nothing was stored.
{
  "message": "(#131026) Message undeliverable",
  "code": "131026",
  "retryable": false
}
404 No such conversation on this account.
{
  "message": "Conversation not found."
}
POST /api/v1/conversations/{conversation}/read Mark the conversation as read (unread count 0), in open panel inboxes as well.
API token

Parameters

conversation
integer · path required
The conversation’s id.

Example request

curl -X POST 'https://podimessage.io/api/v1/conversations/12/read' \
  -H 'Authorization: Bearer podi_xxxxxxxxxxxxxxxx' \
  -H 'Accept: application/json'
<?php
$ch = curl_init('https://podimessage.io/api/v1/conversations/12/read');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer podi_xxxxxxxxxxxxxxxx',
        'Accept: application/json',
    ],
    CURLOPT_POSTFIELDS => '',
]);
$response = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
const res = await fetch('https://podimessage.io/api/v1/conversations/12/read', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer podi_xxxxxxxxxxxxxxxx',
    Accept: 'application/json',
  },
});
const data = await res.json();

Response · HTTP 200

{
  "id": 12,
  "wa_id": "905321234567",
  "name": "Ayşe Yılmaz",
  "phone_number_id": "106540352242922",
  "status": "pool",
  "unread_count": 0,
  "last_message_preview": "Yarın 14:30 uygun mu?",
  "last_message_at": "2026-11-02T10:12:00+03:00",
  "last_inbound_at": "2026-11-02T10:12:00+03:00",
  "can_reply": true,
  "reply_window_expires_at": "2026-11-03T10:12:00+03:00",
  "updated_at": "2026-11-02T10:12:00+03:00"
}

Other responses

404 No such conversation on this account.
{
  "message": "Conversation not found."
}
GET /api/v1/messages/{message}/media Download a message’s photo, video, voice note, document or sticker, streamed from WhatsApp.
API token

Photos, videos and voice notes come inline with their real type; anything else (documents, SVG, …) comes as an application/octet-stream attachment. WhatsApp keeps media for about 30 days.

Parameters

message
integer · path required
The message’s id (as in media.download_path).

Example request

curl 'https://podimessage.io/api/v1/messages/344/media' \
  -H 'Authorization: Bearer podi_xxxxxxxxxxxxxxxx' \
  -H 'Accept: application/json' \
  -o media.jpg
<?php
$ch = curl_init('https://podimessage.io/api/v1/messages/344/media');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer podi_xxxxxxxxxxxxxxxx',
        'Accept: application/json',
    ],
]);
file_put_contents('media.jpg', curl_exec($ch));
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
import { writeFile } from 'node:fs/promises';

const res = await fetch('https://podimessage.io/api/v1/messages/344/media', {
  headers: {
    Authorization: 'Bearer podi_xxxxxxxxxxxxxxxx',
    Accept: 'application/json',
  },
});
await writeFile('media.jpg', Buffer.from(await res.arrayBuffer()));

Response · HTTP 200

The file itself, with these headers:

Content-Type: image/jpeg
Content-Disposition: inline; filename="media-344"
Content-Security-Policy: sandbox; default-src 'none'; img-src 'self' data:; media-src 'self'
X-Content-Type-Options: nosniff
Cache-Control: max-age=300, private

Other responses

404 No such message, or it has no media.
{
  "message": "Message media not found."
}
502 WhatsApp no longer serves the file, for example because it has expired.
{
  "message": "The media could not be fetched from WhatsApp. It may have expired.",
  "code": "100"
}