Skip to main content
Table of Contents

Forwarding messages

Every event WhatsApp posts to the webhook passes through TTMSFNCWhatsAppServer before it reaches your receivers. By default the server forwards each payload unchanged to every connected TTMSFNCWhatsAppReceiver, which suits a single desktop application that shows incoming messages. As soon as there is more than one receiver, more than one business number, or a need to store or audit messages centrally, you want to look at each payload on the server first. This chapter covers inspecting payloads in OnRawMessage, suppressing or filtering the broadcast, routing payloads to selected receivers, and the other traffic the server handles — messages from the receivers themselves and plain HTTP requests.

Inspecting webhook payloads

OnRawMessage fires for every POST that reaches the webhook, with the request body — WhatsApp's JSON payload — in AMessage. Parse it there to log, store, or react to messages on the server. The event runs on the server's connection thread, even when AutoSyncEvents is enabled, so queue any user interface update with TThread.Queue.

Start the webhook server and inspect incoming payloadsPascal
procedure TForm1.FormCreate(Sender: TObject);
begin
  // Callback URL in the Meta dashboard: https://<your-domain>/
  TMSFNCWhatsAppServer1.Port := 443;
  TMSFNCWhatsAppServer1.VerifyToken := 'my-verify-token';   // same value as in the dashboard
  TMSFNCWhatsAppServer1.CertificateFile := 'certs\cert.pem';
  TMSFNCWhatsAppServer1.CertificateKeyFile := 'certs\key.pem';
  TMSFNCWhatsAppServer1.RootCertificateFile := 'certs\chain.pem';
  TMSFNCWhatsAppServer1.Active := True;
end;

procedure TForm1.TMSFNCWhatsAppServer1RawMessage(Sender: TObject;
  AMessage: string; var ABroadcast: Boolean);
begin
  // AMessage is the JSON body WhatsApp posted. This event runs on the
  // server's connection thread, so queue user interface updates.
  TThread.Queue(nil,
    procedure
    begin
      Memo1.Lines.Add(AMessage);
    end);
  // Leave ABroadcast True to forward the payload to every connected receiver.
end;

Filtering what is forwarded

ABroadcast arrives as True; when it is still True after the handler returns, the server broadcasts AMessage to all connected receivers with BroadcastMessage. Set it to False to keep a payload on the server. A common filter forwards only payloads that carry incoming messages and drops delivery and read status updates, which arrive on the same messages field.

Filter webhook payloads before forwardingPascal
uses
  System.JSON;

procedure TForm1.TMSFNCWhatsAppServer1RawMessage(Sender: TObject;
  AMessage: string; var ABroadcast: Boolean);
var
  Json: TJSONValue;
  Field: string;
begin
  // Forward nothing unless the payload proves to be an incoming message.
  ABroadcast := False;

  Json := TJSONObject.ParseJSONValue(AMessage);
  if not Assigned(Json) then
    Exit;
  try
    // {"object":"whatsapp_business_account","entry":[{"changes":[{"field":"messages",
    //   "value":{"messages":[...]} or {"statuses":[...]}}]}]}
    if Json.TryGetValue<string>('entry[0].changes[0].field', Field) and (Field = 'messages') and
       Assigned(Json.FindValue('entry[0].changes[0].value.messages')) then
    begin
      ArchiveMessage(AMessage);   // your own storage, safe to call from a thread
      ABroadcast := True;         // send it on to the connected receivers
    end;
  finally
    Json.Free;
  end;
end;

Routing to selected receivers

To send a payload to some receivers only, set ABroadcast to False and send it yourself with SendMessageTo, whose selector picks the receivers. The inherited BroadcastData and SendDataTo send binary data the same way, and BroadcastMessage can also push messages of your own — for example a notice that the server restarts. Receivers get those messages through their OnMessageReceived event; only WhatsApp-formatted JSON also raises their OnWhatsAppMessageReceived.

Handling messages from receivers

Receivers can send messages to the server too, and the server raises the inherited OnMessageReceived and OnBinaryDataReceived events for them. Use this for a small protocol of your own — a receiver that subscribes to one business number, a receiver that acknowledges a message — and store the result on the connection's UserData (see the server's Custom connection data chapter). The connection events OnConnect, OnAllow, OnDisconnect, OnClose, OnPing, and OnPong work as described in Running a server.

Handling plain HTTP requests

Requests that are neither a WebSocket upgrade nor handled by you receive the server's built-in HTTP response. Assign OnCommandGet to take over that response for every plain HTTP request on the webhook path — for example to serve a health check, or to answer 200 when the webhook uses a custom path (see Webhook setup). The webhook verification answer and payload forwarding still run after your handler.

Combining subscriptions with routed forwarding

The example lets every receiver subscribe to one business phone number, remembers the subscription on its connection, and forwards each payload only to the receivers subscribed to the number that received it.

Forward messages only to subscribed receiversPascal
uses
  System.JSON;

type
  TSubscription = class
  public
    PhoneNumberID: string;
  end;

procedure TForm1.TMSFNCWhatsAppServer1MessageReceived(Sender: TObject;
  AConnection: TTMSFNCWebSocketConnection; const AMessage: string);
var
  Conn: TTMSFNCWebSocketServerConnection;
begin
  // A receiver sends: subscribe:<phone_number_id>
  if AMessage.StartsWith('subscribe:') then
  begin
    Conn := AConnection as TTMSFNCWebSocketServerConnection;
    if not Assigned(Conn.UserData) then
    begin
      Conn.OwnsUserData := True;
      Conn.UserData := TSubscription.Create;
    end;
    TSubscription(Conn.UserData).PhoneNumberID := AMessage.Substring(Length('subscribe:'));
  end;
end;

procedure TForm1.TMSFNCWhatsAppServer1RawMessage(Sender: TObject;
  AMessage: string; var ABroadcast: Boolean);
var
  Json: TJSONValue;
  PhoneNumberID: string;
begin
  // Do not broadcast to everyone; route by the business number that received it.
  ABroadcast := False;
  Json := TJSONObject.ParseJSONValue(AMessage);
  if not Assigned(Json) then
    Exit;
  try
    if not Json.TryGetValue<string>('entry[0].changes[0].value.metadata.phone_number_id',
      PhoneNumberID) then
      Exit;
  finally
    Json.Free;
  end;

  TMSFNCWhatsAppServer1.SendMessageTo(AMessage,
    function(AConnection: TTMSFNCWebSocketServerConnection): Boolean
    begin
      Result := Assigned(AConnection.UserData) and
        (TSubscription(AConnection.UserData).PhoneNumberID = PhoneNumberID);
    end);
end;

Pitfalls

  • OnRawMessage runs on a background thread. Never touch controls from it directly, whatever AutoSyncEvents is set to.
  • Setting ABroadcast to False stops all forwarding. Send the payload yourself when some receivers should still get it.
  • Status updates share the messages field. Filter on the presence of messages in the payload when only incoming messages matter.
  • Answer quickly. Meta expects a prompt response to each POST; move slow work such as database writes out of OnRawMessage when it could delay the reply.
  • Receivers ignore other JSON. Messages you send yourself reach the receivers' OnMessageReceived, but do not raise OnWhatsAppMessageReceived unless they follow WhatsApp's payload format.

See also