Skip to content
PaalChat Docs

Common workflows

Receiving messages

Build an inbox from message.received callbacks.

.md

In a nutshell

Every WhatsApp message a customer sends arrives once as a message.received callback with Meta's message object. PaalChat does not keep the text, so store it. It also opens the 24-hour window for replies.

The callback

message.received arrives once per WhatsApp message:

JSON
{
  "id": "7f0c2d9e-5a41-4f7b-9b0e-3c1d2e4f5a6b",
  "event": "message.received",
  "occurred_at": "2026-09-30T18:54:28+00:00",
  "business": {"external_id": "presec"},
  "data": {
    "message_id": 57,
    "wamid": "wamid.HBgMMjMzMjQxMjM0NTY3FQIAEhgg",
    "from": "233241234567",
    "profile_name": "Ama Mensah",
    "phone_number_id": "106540352242922",
    "type": "text",
    "timestamp": "1759240000",
    "message": {
      "from": "233241234567", "id": "wamid.HBgMMjMzMjQxMjM0NTY3FQIAEhgg", "timestamp": "1759240000",
      "type": "text", "text": {"body": "Good morning. Is there school on Friday?"}
    }
  }
}

Important

PaalChat keeps the conversation history, encrypted, for 365 days by default (details). Store what you need for longer yourself.

Message types

data.message is Meta's message object, unchanged:

type Where the content is
text message.text.body
button message.button.text, message.button.payload (quick reply on a template)
interactive message.interactive.button_reply or list_reply (id, title)
image, document, audio, video, sticker message.<type>.id (Meta's ID), mime_type, caption; PaalChat's copy is media_id - see Media
location message.location.latitude, longitude, name, address
reaction message.reaction.message_id, emoji
contacts message.contacts[]
unsupported nothing usable

message.context.id is present when the customer replied to a specific message - it is that message's wamid.

Build an inbox

  1. Store it. Save data.message under contact data.from, keyed by data.wamid so a repeated callback does not add a duplicate. Show data.profile_name.
  2. Note the window. The contact can now receive free-form text for 24 hours - from the number they wrote to (data.phone_number_id).
  3. Reply. Send type: text to data.from from that phone_number_id, with your own reference.
  4. Fall back. On 422 outside_service_window, offer the agent a template instead.
// routes/api.php: Route::post('/paalchat/callback', PaalChatCallbackController::class);
public function __invoke(Request $request)
{
    abort_unless(paalchatSignatureValid(
        $request->header('X-PaalChat-Signature', ''), $request->getContent(), config('services.paalchat.callback_secret'),
    ), 401);

    $event = $request->json()->all();

    if ($event['event'] === 'message.received') {
        InboxMessage::firstOrCreate(['wamid' => $event['data']['wamid']], [
            'contact' => $event['data']['from'],
            'name' => $event['data']['profile_name'],
            'type' => $event['data']['type'],
            'body' => $event['data']['message']['text']['body'] ?? null,
            'payload' => $event['data']['message'],
        ]);
    }

    return response()->noContent();
}

The signature helpers are on the Callbacks page.