External AI Integration

Let your own AI answer customers — receive unanswered inbound messages via webhook and reply through the API.

External AI Integration

Connect your own AI to Wax so it can answer inbound customer messages over WhatsApp or RCS. Wax calls your webhook when a message reaches the end of your automations unanswered, and your AI replies through the API.

Looking to mirror conversations into a help desk instead? This page describes a request/response integration: Wax asks your service for an answer. If you want a one-way feed of every message on a conversation that needs attention — quick replies, media and the automated messages your flows send included — see External Helpdesk Integration. The two are independent and can run side by side.

Configure this integration in Settings → Organization → External AI.

Overview

The integration works in two steps:

  1. Inbound webhook -- Wax forwards unanswered customer messages to your URL
  2. Reply endpoint -- Your system sends a response back via the Wax API

1. Inbound Message Webhook

Wax calls your webhook only for messages nothing else handled. A message is skipped when it:

  • matches a quick reply, list option or other flow node, so a flow continues
  • answers a step that was waiting for an answer
  • matches a keyword that triggers a flow
  • is detected as an opt-out and an opt-out flow is active

Everything left over -- typically free-text questions, plus media messages -- is forwarded to your webhook URL. Because this integration is meant to answer the customer, receiving a message also interrupts any flow the contact was in and marks the conversation unresolved in the Wax inbox.

Request

POST <your_webhook_url>
Content-Type: application/json
X-Signature: <HMAC-SHA256 signature>

Signature Verification

Every webhook request includes an X-Signature header containing an HMAC-SHA256 hex digest of the raw JSON body, signed with your organization's bearer token.

To verify the signature:

import hmac
import hashlib

def verify_signature(body: bytes, signature: str, bearer_token: str) -> bool:
    expected = hmac.new(
        bearer_token.encode(),
        body,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)
const crypto = require('crypto');

function verifySignature(body, signature, bearerToken) {
  const expected = crypto
    .createHmac('sha256', bearerToken)
    .update(body)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}
expected = OpenSSL::HMAC.hexdigest('SHA256', bearer_token, raw_body)
valid = ActiveSupport::SecurityUtils.secure_compare(expected, signature)

Payload Structure

{
  "message_id": "wamid_ABCdef123456",
  "organization_id": 42,
  "contact_id": 789,
  "message": {
    "id": "whatsapp:48213",
    "external_message_id": "wamid_ABCdef123456",
    "channel": "whatsapp",
    "content": "How is my order?",
    "timestamp": "2026-07-08T14:03:21.532Z",
    "media_url": "https://...",
    "media_type": "image/jpeg"
  },
  "contact": {
    "name": "John Doe",
    "phone": "+33612345678",
    "email": "[email protected]",
    "tags": ["vip", "returning_customer"],
    "custom_attributes": {
      "loyalty_tier": "gold",
      "preferred_language": "fr"
    }
  },
  "history": [
    {
      "id": "whatsapp:48201",
      "external_message_id": "wamid_XYZabc987654",
      "role": "user",
      "content": "Hi there",
      "timestamp": "2026-07-08T13:58:02.101Z"
    },
    {
      "id": "whatsapp:48204",
      "external_message_id": "wamid_QRSdef456789",
      "role": "assistant",
      "content": "Hello! How can I help you today?",
      "timestamp": "2026-07-08T13:58:40.870Z"
    }
  ],
  "shopify": {
    "recent_orders": [
      {
        "order_number": "#1234",
        "status": "shipped",
        "items": ["White Sneakers", "Black T-Shirt"],
        "date": "2026-03-20T10:30:00Z"
      }
    ],
    "active_checkouts": [
      {
        "items": ["Blue Jacket"],
        "created_at": "2026-03-27T09:00:00Z"
      }
    ]
  }
}

Field Reference

FieldTypeDescription
message_idstringWhatsApp message ID (wamid)
organization_idintegerYour Wax organization ID
contact_idintegerWax contact ID -- use this to send replies
message.idstringStable unique message ID, prefixed with the channel (whatsapp:123, rcs:45, sms:67) -- use it to deduplicate
message.external_message_idstring?Provider-side message ID (e.g. the WhatsApp wamid)
message.channelstringwhatsapp, rcs, or sms
message.contentstringText content of the message
message.timestampstringISO8601 with millisecond precision
message.media_urlstring?URL of attached media. Images only -- video, audio and document messages carry no media fields on this webhook (the helpdesk webhook sends all four)
message.media_typestring?Present with media_url. Currently always reported as image/jpeg, so do not rely on it to detect the real file type -- read the extension of media_url instead
contact.namestring?Contact display name
contact.phonestringPhone number in E.164 format
contact.emailstring?Contact email address
contact.tagsstring[]Tag names assigned to the contact
contact.custom_attributesobject?Key-value pairs of custom attributes
historyarrayLast 20 messages (excluding current), sorted chronologically ascending
history[].idstringStable unique message ID, same format as message.id
history[].external_message_idstring?Provider-side message ID
history[].rolestringuser (the contact) or assistant (sent by Wax or your integration)
history[].contentstringText content of the message
history[].timestampstringISO8601 with millisecond precision
shopifyobject?null unless Shopify is connected and "Include Shopify data" is enabled on the webhook (Settings → Organization → External AI)
shopify.recent_ordersarrayLast 5 orders with status: "ordered", "shipped", "delivered", or "cancelled"
shopify.active_checkoutsarrayUp to 3 incomplete checkouts from the last month

Storing Messages & Deduplication

If you persist conversations on your side (e.g. as help desk tickets), use id as the primary key for each message:

  • message.id and history[].id are stable across deliveries -- the same message always carries the same ID.
  • Deliveries are at-least-once: failed requests are retried, and history intentionally overlaps between deliveries. Upsert on id rather than appending.
  • Messages sent by human agents from the Wax inbox appear in history with role: "assistant" on the next inbound message, so you can backfill them deduped.
  • Use timestamp (millisecond precision) to order messages; history is already sorted ascending.

Expected Response

Return a 2xx status code to acknowledge receipt. The response body is ignored.

If your endpoint returns a non-2xx status or times out (5 seconds), Wax retries up to 5 times with exponential backoff. After all retries are exhausted, the conversation is marked as unresolved in the inbox for a human agent to handle.

Rapid-Fire Messages

Your webhook is called as soon as a message is processed, with no artificial delay. Calls for the same conversation are collapsed within a 20-second window, so a customer sending several messages in quick succession produces one call rather than several. That call describes the most recent message in message, with the earlier ones present in history -- so read history, not just message, if you need the customer's full thought.

Custom Headers

You can configure custom headers on your webhook (e.g., for additional authentication). These are sent with every webhook request alongside the standard Content-Type and X-Signature headers.


2. Reply Endpoint

API Reference: POST /v1/conversation_replies

Send a plain text reply to a contact. The message is delivered via the contact's default channel (WhatsApp or RCS).

Request

POST https://api.getwax.io/v1/conversation_replies
Authorization: Bearer <your_organization_bearer_token>
Content-Type: application/json
{
  "contact_id": 789,
  "message_body": "Your order #1234 has been shipped and should arrive by Friday!"
}

Parameters

ParameterTypeRequiredDescription
contact_idintegeryesThe Wax contact ID (from the webhook payload)
message_bodystringyesPlain text message, max 4096 characters

Channel Routing

The message is automatically sent via the contact's default channel:

  • WhatsApp -- if the contact last messaged via WhatsApp (or has no channel set)
  • RCS -- if the contact last messaged via RCS

You do not need to specify the channel -- Wax handles routing automatically.

Response

Success (200):

{
  "success": true,
  "message_id": "whatsapp:48214"
}
FieldTypeDescription
successbooleanAlways true on a 200
message_idstring?ID of the message Wax created for your reply, in the same channel:id form as message.id on the webhook. null when the contact is opted out of the channel, in which case nothing was sent. Store it if you also use the helpdesk webhook, which mirrors your own replies back to you -- matching on this ID lets you drop the echo instead of duplicating the message in your thread

Errors:

StatusDescription
401Invalid or missing bearer token
404Contact not found
422Validation error (missing message_body, message too long, or channel not configured)

Error response format:

{
  "errors": ["Contact not found"]
}

Example

const response = await fetch('https://api.getwax.io/v1/conversation_replies', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer <your_bearer_token>',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    contact_id: 789,
    message_body: 'Your order #1234 has been shipped!'
  })
});
import requests

response = requests.post(
    'https://api.getwax.io/v1/conversation_replies',
    headers={
        'Authorization': 'Bearer <your_bearer_token>',
        'Content-Type': 'application/json'
    },
    json={
        'contact_id': 789,
        'message_body': 'Your order #1234 has been shipped!'
    }
)
require 'faraday'

conn = Faraday.new(url: 'https://api.getwax.io')
response = conn.post('/v1/conversation_replies') do |req|
  req.headers['Authorization'] = 'Bearer <your_bearer_token>'
  req.headers['Content-Type'] = 'application/json'
  req.body = {
    contact_id: 789,
    message_body: 'Your order #1234 has been shipped!'
  }.to_json
end

Full Integration Flow

Customer sends WhatsApp message
        |
        v
  Wax receives message
        |
        v
  Flows, keywords and waiting steps get first refusal
  (if one of them handles it, your webhook is NOT called)
        |
        v
  POST to your webhook URL
  (with message + contact + history + shopify context)
        |
        v
  Your AI processes the message
        |
        v
  POST /v1/conversation_replies
  (with contact_id + message_body)
        |
        v
  Wax sends reply via WhatsApp/RCS
  Timeline remains unresolved in inbox