Skip to content

Everything for WordPress, web development — and beyond

🤖 Viber chatbot in PHP: sending structured messages with keyboard menus

🤖 Viber chatbot in PHP: sending structured messages with keyboard menus

The user presses a button in the chat and receives not bare text but a grid of images, links, and colored keys. It looks like a native app. In reality, it is plain JSON that your PHP script returns via the Viber REST API.

The problem with most tutorials: they teach you to respond with text to text. A real business bot must display a keyboard menu with link buttons, images, and carousels. Otherwise the user simply will not understand what the bot can do and will leave. Since February 2024, Viber has put bot creation on a commercial basis (100 € per month through official Rakuten Viber partners), and authentication now goes into the HTTP header X-Viber-Auth-Token rather than into the request body. Snippets with auth_token inside JSON no longer work.

Here is a working Viber bot in PHP from scratch: from setting up the webhook to sending a structured keyboard menu with reply and open-url buttons. The code is current for API version 7.3.

💡 Quick overview:

  • Register a commercial bot through a Viber partner and obtain an authentication token
  • Configure the webhook via set_webhook with the X-Viber-Auth-Token header
  • Receive callback events from Viber: webhook → subscribed → message
  • Build a keyboard menu (a button grid with custom colors, sizes, and actions)
  • Send a structured response: text, images, links, carousels via send_message

How Viber Bot API works

Viber Bot API operates on a REST model: your server receives callback requests on the webhook and responds by calling https://chatapi.viber.com/pa/send_message. Every request is authenticated with a token in the X-Viber-Auth-Token header.

The basic cycle looks like this:

  • You register a bot through a Viber partner and receive a token in the "Edit Info" panel.
  • You configure a webhook (the URL of your server with a valid SSL; Let's Encrypt works since it is in Viber's trusted Java certificate list; self-signed certificates are not accepted).
  • Viber sends POST requests to this URL on every event: a user subscribed, sent a message, pressed a button.
  • Your PHP script reads the incoming JSON, parses the event field, and responds by calling send_message.

The first step to understanding is to look at a live bot. Open Viber and search for a public account of any well-known brand. Almost every one has a chatbot with a menu. The screenshot below shows a typical public account search result.

Searching for a public account in Viber

After subscribing you enter a one-on-one chat. It looks roughly like this: an avatar, a welcome message, and a button to start the conversation.

Viber public chat screen after subscribing

Tap the message icon in the upper right corner and send "Hello." If the bot is configured with a keyboard menu, you will see a response with a button grid:

Keyboard menu with buttons in a Viber chat

This is a structured message. There are two button types: reply sends text back to the bot (pressing "News" or "Articles"), while open-url opens a link in the browser. Now let us write the code that produces this.

Step 1: Obtain the token and configure the webhook

The token is located in the Viber admin panel: Edit Info section → App Key field. It is a string of letters and numbers that you include in every API request.

The webhook is set with a single POST request to https://chatapi.viber.com/pa/set_webhook. The request body is JSON with the URL of your handler and a list of events you subscribe to. The header is X-Viber-Auth-Token with your token.

Webhook setup script (save as setup.php and run once):

1<?php
2$token = 'ВАШ_X_VIBER_AUTH_TOKEN';
3
4$data = json_encode([
5 'url' => 'https://your-domain.com/webhook.php',
6 'event_types' => ['message', 'subscribed', 'conversation_started'],
7]);
8
9$ch = curl_init('https://chatapi.viber.com/pa/set_webhook');
10curl_setopt($ch, CURLOPT_POST, 1);
11curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
12curl_setopt($ch, CURLOPT_HTTPHEADER, [
13 'Content-Type: application/json',
14 'X-Viber-Auth-Token: ' . $token,
15]);
16curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
17
18$result = curl_exec($ch);
19$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
20curl_close($ch);
21
22echo "HTTP {$httpCode}: {$result}\n";
23
24if ($httpCode === 200) {
25 $response = json_decode($result, true);
26 if (($response['status'] ?? -1) === 0) {
27 echo "Вебхук установлен успешно.\n";
28 } else {
29 echo "Ошибка Viber: " . ($response['status_message'] ?? 'неизвестная') . "\n";
30 }
31}

What happens here: cURL sends a POST to set_webhook. The token is strictly in the header, not in the body. Viber returns {"status":0} on success and immediately sends a webhook callback to the specified URL to verify its availability. Responding with {"status":0,"status_message":"ok"} to this callback is mandatory (we will cover this in the next step).

If you receive invalidUrl, check your SSL certificate. Let's Encrypt works; self-signed does not. On your server it is enough to run certbot --nginx -d your-domain.com and set up auto-renewal via cron.

Step 2: Receive callback events from Viber

When a user interacts with the bot, Viber sends a POST request with JSON to your webhook. Your task is to read the event field and react.

A minimal handler webhook.php that correctly responds to all events:

1<?php
2$request = file_get_contents("php://input");
3$input = json_decode($request, true);
4
5if ($input['event'] === 'webhook') {
6 $response = [
7 'status' => 0,
8 'status_message' => 'ok',
9 'event_types' => ['delivered', 'seen', 'message', 'subscribed', 'conversation_started'],
10 ];
11 echo json_encode($response);
12 exit;
13}
14
15if ($input['event'] === 'subscribed') {
16 $sender_id = $input['sender']['id'];
17 sendWelcomeMessage($sender_id);
18}
19
20if ($input['event'] === 'conversation_started') {
21 $sender_id = $input['sender']['id'];
22 sendMainMenu($sender_id);
23}
24
25if ($input['event'] === 'message') {
26 $type = $input['message']['type'];
27 $text = $input['message']['text'];
28 $sender_id = $input['sender']['id'];
29 $sender_name = $input['sender']['name'];
30
31 $data = match ($text) {
32 'News' => getNewsList($sender_id),
33 'Articles' => getArticleList($sender_id),
34 'Gallery' => getGalleryList($sender_id),
35 default => getMainMenu($sender_id),
36 };
37
38 sendToViber($data);
39}

Event breakdown:

  • webhook arrives once when the webhook is set. You must return {"status":0,"status_message":"ok"} with a list of supported events. Without this, Viber will not confirm the URL and the bot will not start.
  • subscribed means a user has subscribed to the bot. This is the ideal moment to send a welcome menu.
  • conversation_started means a user opened the chat (for the first time or again). Also suitable for showing the main menu.
  • message is the main working event. The text field contains either an arbitrary user message or the ActionBody of a pressed button (if its type is reply). This is how the bot understands what was selected: "News," "Articles," or "Gallery."

The match() construct is available from PHP 8. If you have PHP 7.4, replace it with switch.

Step 3: Build the keyboard menu

A keyboard is attached to any message via the keyboard field in the send_message JSON object. It is an array of buttons with separate settings for width (Columns, 1-6), height (Rows, 1-2), background color, text, and action.

A function that assembles a main menu from seven reply buttons and one link button:

1<?php
2
3function getMainMenu(string $user_id): array
4{
5 $buttons = [];
6
7 $addReplyButton = function (string $label, string $actionBody, int $cols = 2, int $rows = 2) use (&$buttons) {
8 $buttons[] = [
9 'Columns' => $cols,
10 'Rows' => $rows,
11 'Text' => $label,
12 'TextSize' => 'regular',
13 'TextVAlign' => 'bottom',
14 'TextHAlign' => 'center',
15 'TextOpacity' => 100,
16 'ActionType' => 'reply',
17 'ActionBody' => $actionBody,
18 'BgColor' => '#FFFFFF',
19 ];
20 };
21
22 $addReplyButton('NEWS', 'News');
23 $addReplyButton('ARTICLES', 'Articles');
24 $addReplyButton('INTERVIEWS', 'Interviews');
25 $addReplyButton('GALLERY', 'Gallery');
26 $addReplyButton('POLL', 'Poll');
27 $addReplyButton('PLAYER OF THE MONTH','POTM');
28 $addReplyButton('QUOTE OF THE DAY', 'Quote');
29
30 $buttons[] = [
31 'Columns' => 4,
32 'Rows' => 2,
33 'Text' => 'VISIT OUR WEBSITE',
34 'TextSize' => 'regular',
35 'TextVAlign' => 'bottom',
36 'TextHAlign' => 'center',
37 'TextOpacity' => 100,
38 'ActionType' => 'open-url',
39 'ActionBody' => 'https://your-site.com',
40 'BgColor' => '#FFFFFF',
41 ];
42
43 return [
44 'receiver' => $user_id,
45 'type' => 'text',
46 'text' => 'Please select one of the options below:',
47 'keyboard' => [
48 'Type' => 'keyboard',
49 'BgColor' => '#FFFFFF',
50 'Buttons' => $buttons,
51 ],
52 ];
53}

Buttons with Columns=2 and Rows=2 are square, three per row (2+2+2=6). The link button with Columns=4 occupies its own row. The grid is assembled left to right, top to bottom. The sum of Columns in a row determines the layout.

Button parameters:

Parameter

Values

Purpose

Columns

1-6

Button width in conditional columns

Rows

1-2

Button height in rows

ActionType

reply or open-url

reply returns text to the bot, open-url opens the browser

ActionBody

string

For reply the text the bot will receive back; for open-url the URL

Text

string

Label on the button, supports HTML tags b, i, and font color

BgColor

HEX

Button background color

Image

URL

Image on top of the button, JPEG, optional

Step 4: Send data via send_message

A sender function that you call from the event handler:

1<?php
2
3function sendToViber(array $data): void
4{
5 $token = 'ВАШ_X_VIBER_AUTH_TOKEN';
6
7 $ch = curl_init('https://chatapi.viber.com/pa/send_message');
8 curl_setopt($ch, CURLOPT_POST, 1);
9 curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
10 curl_setopt($ch, CURLOPT_HTTPHEADER, [
11 'Content-Type: application/json',
12 'X-Viber-Auth-Token: ' . $token,
13 ]);
14 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
15
16 $result = curl_exec($ch);
17 $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
18 curl_close($ch);
19
20 if ($httpCode !== 200) {
21 error_log("Viber API error: HTTP {$httpCode} - {$result}");
22 }
23}

The key difference from old tutorials: the token is in the header, not in the JSON body. The body contains only the fields receiver, type, text, keyboard, and optionally sender.name, sender.avatar, tracking_data. The maximum JSON size is 30 KB. If you exceed it, Viber silently discards the message; check strlen(json_encode($data)) before sending.

Step 5: Assemble content for buttons

When a user presses a reply button, its ActionBody is sent to the webhook as message text. You parse it in match() and call the appropriate function. Here is an example for a news list with images:

1<?php
2
3function getNewsList(string $user_id): array
4{
5 $newsItems = [
6 [
7 'title' => 'Your Article Title',
8 'link' => 'https://your-site.com/article-1',
9 'image' => 'https://your-site.com/images/article-1.jpg',
10 ],
11 [
12 'title' => 'Another Article',
13 'link' => 'https://your-site.com/article-2',
14 'image' => 'https://your-site.com/images/article-2.jpg',
15 ],
16 ];
17
18 $buttons = [];
19 foreach ($newsItems as $item) {
20 $buttons[] = [
21 'Columns' => 2,
22 'Rows' => 2,
23 'ActionType' => 'open-url',
24 'ActionBody' => $item['link'],
25 'BgColor' => '#FFFFFF',
26 'Image' => $item['image'],
27 ];
28 $buttons[] = [
29 'Columns' => 4,
30 'Rows' => 2,
31 'Text' => $item['title'],
32 'TextSize' => 'regular',
33 'TextHAlign' => 'left',
34 'TextVAlign' => 'top',
35 'ActionType' => 'open-url',
36 'ActionBody' => $item['link'],
37 'BgColor' => '#F5F5F5',
38 ];
39 }
40
41 return [
42 'receiver' => $user_id,
43 'type' => 'text',
44 'text' => 'Here are the latest updates:',
45 'keyboard' => [
46 'Type' => 'keyboard',
47 'BgColor' => '#DDDDDD',
48 'Buttons' => $buttons,
49 ],
50 ];
51}

On the user's side this looks like a grid of cards: image on the left, title on the right. Tapping any part opens the link in the browser.

Rich message with images and links in Viber

A gallery, article list, or polls are assembled in exactly the same way; only the contents of the $buttons array and the message text change. If there is a lot of content and the JSON approaches 30 KB, split it into pages with a "Next" button.

If you prefer working with a ready-made PHP library rather than the raw API, check out viber-bot-php by Bogdaan. It handles signature validation, event routing, and JSON generation for you.

Summary: full launch sequence

  • Get the token in the Viber Admin Panel: Edit Info section → App Key.
  • Deploy the handler to a server with HTTPS. Let's Encrypt works; a self-signed certificate does not. Viber validates certificates against the trusted Java Root CA list.
  • Set the webhook with a POST request to https://chatapi.viber.com/pa/set_webhook with body {"url":"https://your-domain/webhook.php","event_types":[...]} and header X-Viber-Auth-Token.
  • Check the response: {"status":0} means success. Viber will immediately send a webhook callback to your URL. Make sure the script returns {"status":0,"status_message":"ok"}.
  • Send the first menu: on the conversation_started event call getMainMenu() and pass the result to sendToViber().

⁉️🤔 Frequently asked questions

Can I use a self-signed SSL certificate for the webhook?

No. Viber validates the certificate against the list of trusted Java root certificate authorities. A self-signed certificate causes an invalidUrl error when setting the webhook. Let's Encrypt is in this list and is free. On your server, run certbot --nginx -d your-domain.com, add auto-renewal to cron, and Viber will accept the certificate without issues.

What is the difference between reply and open-url in ActionType?

reply sends ActionBody back to the webhook as message text. The PHP script sees it in $input['message']['text'] and routes it via match(). Use this for navigation within the bot. open-url opens a link in an external browser and does not trigger the webhook. In practice, menus combine both types: five or six reply buttons for sections and one wide open-url button for navigating to the website.

What message types does Viber Bot API support?

Viber Bot API supports nine type values in send_message: text (plain text), picture (JPEG image), video (video file), file (any file up to 50 MB), location (geo point), contact (contact card), sticker (sticker), rich_media (card carousel), and url (link preview). For the current list with required fields, see the Viber Developers Hub documentation. The most common scenario is text + keyboard for menus and picture for sending images. Carousels are great for storefronts and catalogs but require more code.

What should I do if the user does not see the keyboard?

There are three common causes. First: the JSON with the keyboard exceeded 30 KB, and Viber silently discards such messages. Check strlen(json_encode($data)) before sending. Second: the response to set_webhook did not contain "status":0, the webhook was not set, and the bot is not receiving messages. Third: you are using the old format with auth_token in the JSON body. Since API 7.0 this is ignored; you need the X-Viber-Auth-Token header. For debugging, enable logging of the Viber API response: it returns readable JSON with error codes invalidAuthToken, badData, or missingData.

Can I change the keyboard after sending?

Yes. With each new message you can send a different keyboard. The Viber client always shows the last one received. This lets you build multi-level menus: main menu → section submenu → specific content. Each level has its own set of buttons, and the user moves between them by pressing reply keys.

How much does launching a Viber bot cost in 2026?

Since February 5, 2024, creating a new bot costs 100 € per month through official Rakuten Viber partners. Bots created before that date continue under the old terms. The cost includes a dedicated account with an admin panel, analytics, and access to all message types including rich_media and Viber Pay. The technical part of the API is identical for commercial and legacy free bots.

The bot is ready: what comes next

A keyboard menu is only an entry point. Once the basic mechanics are working, add content carousels via rich_media, payments via Viber Pay, and user segmentation based on which buttons they press most often. Each new level does not change the foundation: you still read event and still send send_message with the X-Viber-Auth-Token header.

If you need a live example of working code, check out the viber-bot-php repository on GitHub. It covers all events, request signing, and building a keyboard with images. And for a visual start, here is a half-hour tutorial on creating a Viber bot from scratch:

The main thing to remember: Viber Bot API is simple at the start but demanding about details. A correct authentication header, trusted SSL, valid JSON under 30 KB, and clear ActionBody routing are enough to keep the bot running without failures. Take the snippets above, substitute your token and domain, and launch your first menu today.