API documentation
Standard account
API documentation
Send your WhatsApp messages from your own software: approved templates and verification codes, scheduled campaigns and inbox replies.
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/sendanswer returns it ascostand the balance left asbalance. -
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/mereturns 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/jsonandAccept: 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) andpageand returnmetawithcurrent_page,last_pageandtotal. 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).
balance, and required where it is known.
Allow header.
message and code).
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_keyin 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 withcode: idempotency_key_reused. -
conversations/{id}/reply: theIdempotency-Keyheader (oridempotency_keyin the body), per conversation. A replayed answer carriesIdempotent-Replayed: true; while the first request is still running you get 409 withcode: in_progress. -
otp/sendandcampaignstake no key: check the outcome before you retry them.
Quick start
From your API token to a two-way conversation in five steps.
-
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
Check your token
Returns the account the request runs as and your balance.
Example request —
GET /api/v1/mecurl '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
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/sendcurl -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
Get the replies
When the customer writes back, the conversation is updated. Poll
GET /api/v1/conversationswithupdated_sinceand fetch a conversation’s new messages withafter_id.Example request —
GET /api/v1/conversationscurl '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
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}/replycurl -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
name
string
template
string
language
string
category
string
marketing, utility or authentication.status
string
scheduled, queued, processing, completed, failed or cancelled.scheduled_at
string|null
null when sent at once.total_recipients
integer
sent
integer
failed
integer
delivered
integer
read
integer
started_at
string|null
completed_at
string|null
GET
/api/v1/me
Check your token: returns the account the request runs as and its balance.
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.
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
template
language
tr or en_US. The default is en_US, so set it for Turkish templates.params
{{1}}, {{2}}… in the template body, in order.phone_number_id
idempotency_key
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
{
"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"
}
phone_number_id you named).
{
"message": "No active WhatsApp number is connected. Connect one in the panel."
}
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.
Parameters
to
template
code
language
tr or en_US. The default is en_US, so set it for Turkish templates.include_button
true. Set it to false if the template has no button.button_sub_type
url (default; works for one-tap and copy-code buttons) or copy_code.phone_number_id
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
phone_number_id you named).
{
"message": "No active WhatsApp number is connected. Connect one in the panel."
}
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).
Parameters
status
ALL (default), APPROVED, PENDING, REJECTED, PAUSED or DISABLED.phone_number_id
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
phone_number_id you named).
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.
Parameters
name
_, up to 512 characters.language
tr.category
MARKETING, UTILITY or AUTHENTICATION.components
example.phone_number_id
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
phone_number_id you named).
code is Meta’s error code.
DELETE
/api/v1/templates/{name}
Delete a template on Meta.
Parameters
name
phone_number_id
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
phone_number_id you named).
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.
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
template
language
recipients
recipients.*.phone
recipients.*.name
recipients.*.params
{{1}}, {{2}}… for this recipient.scheduled_at
phone_number_id
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
{
"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"
}
phone_number_id you named).
{
"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.
Parameters
campaign
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
{
"message": "Campaign not found."
}
GET
/api/v1/campaigns
Your campaigns (from the panel and the API), newest first.
Parameters
status
scheduled, queued, processing, completed, failed or cancelled.per_page
page
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.
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
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
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
wa_id
string
name
string|null
phone_number_id
string|null
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
last_message_preview
string|null
last_message_at
string|null
last_inbound_at
string|null
can_reply
boolean
true while the 24-hour reply window is open and the number is active.reply_window_expires_at
string|null
null if the customer has never written.updated_at
string|null
updated_since.Message object
id
integer
direction
string
inbound (from the customer) or outbound.type
string
text, image, video, audio, document, sticker, location, button, interactive…text
string|null
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
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
GET
/api/v1/conversations
Your conversations, most recent first.
Parameters
status
pool (in Waiting, not yet taken by anyone), assigned, closed or all (default).updated_since
wa_id
per_page
page
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.
Parameters
conversation
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
{
"message": "Conversation not found."
}
GET
/api/v1/conversations/{conversation}/messages
A conversation’s messages, newest first.
To page back, pass meta.next_before_id as before_id while has_more is true.
Parameters
conversation
id.per_page
before_id
after_id
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
{
"message": "Conversation not found."
}
POST
/api/v1/conversations/{conversation}/reply
Answer with free text inside the 24-hour reply window. Not charged.
Parameters
conversation
id.text
Idempotency-Key
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
{
"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"
}
{
"message": "No active WhatsApp number is connected. Connect one in the panel.",
"code": "no_account"
}
Idempotency-Key is still running.
{
"message": "A request with this idempotency key is still being processed. Try again shortly.",
"code": "in_progress"
}
retryable says whether trying again may help. Nothing was stored.
{
"message": "(#131026) Message undeliverable",
"code": "131026",
"retryable": false
}
{
"message": "Conversation not found."
}
POST
/api/v1/conversations/{conversation}/read
Mark the conversation as read (unread count 0), in open panel inboxes as well.
Parameters
conversation
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
{
"message": "Conversation not found."
}
GET
/api/v1/messages/{message}/media
Download a message’s photo, video, voice note, document or sticker, streamed from WhatsApp.
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
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
{
"message": "Message media not found."
}
{
"message": "The media could not be fetched from WhatsApp. It may have expired.",
"code": "100"
}