¶ REST API
Send messages, look up contacts and tags on demand. Use it to push actions and pull data.
Source: docs/public-api.md
The ReplyBox API lets you send WhatsApp messages, read your contacts and tags, and subscribe to real-time events — programmatically, from your own systems (a CRM, an automation platform such as n8n/Zapier/Make, or a custom backend).
https://api.replybox.utopiagroup.com.my/api/v1
FormatJSON over HTTPS
AuthBearer API token
Send messages, look up contacts and tags on demand. Use it to push actions and pull data.
Receive an HTTP callback the moment something happens (a message arrives, a message is sent, a contact is tagged). Use them to receive events without polling.
Most integrations use both — webhooks for real-time updates, and the REST API for backfill and reconciliation.
https://api.replybox.utopiagroup.com.my/api/v1
Note: If your workspace uses a custom API domain, replace the host above with the one provided to you. Every path in this document is relative to /api/v1.
All requests and responses use JSON (Content-Type: application/json) and must be made over HTTPS.
Getting started
The REST API authenticates with a per-organization API token, sent as a bearer token in the Authorization header:
Authorization: Bearer rb_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Every token is scoped to a single organization (workspace). ReplyBox resolves your organization from the token — you never pass an organization ID to the public API.
Sign in to the ReplyBox dashboard.
Open Settings → Developers → API Token.
Click Generate Token, give it a name (e.g. crm-sync), and copy it.
A token looks like rb_live_ followed by a random string. Treat it like a password: anyone holding it can send messages and read data for your workspace.
Delete a token from the same API Token screen. Revocation takes effect immediately; any request using a revoked token returns 401 Unauthorized.
The token is shown only once. Store it in a secrets manager or environment variable. If you lose it, revoke it and generate a new one. Generating a token requires the api:manage permission.
Getting started
Organization, channel, contact, tag, message, webhook IDs are UUIDs (e.g. 3f2a…).
Phone numbers are supplied in international format without a +, spaces, or dashes — digits only (e.g. 60123456789). You may also pass a full WhatsApp JID (e.g. 60123456789@s.whatsapp.net).
List endpoints that can return many rows use limit and offset query parameters and return a total count for the current filter. See List contacts.
Errors use standard HTTP status codes and a JSON body:
{ "detail": "channel not found" }
| Status | Meaning |
|---|---|
400 Bad Request | Malformed request or invalid field (e.g. bad phone number). |
401 Unauthorized | Missing, invalid, or revoked API token. |
404 Not Found | The resource doesn't exist in your organization. |
409 Conflict | The channel isn't connected. |
413 Payload Too Large | Media exceeds the size limit (100 MB). |
422 Unprocessable Entity | Validation failed (e.g. unsupported message_type). |
502 Bad Gateway | A downstream dependency (media fetch, storage) failed. |
You → ReplyBox
/public/messagesSend a WhatsApp message (text or media) to any phone number. If the recipient isn't already a contact, ReplyBox creates the contact and conversation automatically.
| Field | Type | Required | Description |
|---|---|---|---|
channelId | string (UUID) | Yes | The connected WhatsApp channel to send from. |
to | string | Yes | Recipient phone number (digits only) or JID. |
message_type | string | No | One of text (default), image, video, document, audio, voice. |
body | object | Yes | Message content — see below. |
body.text | string | For text | The message text. Max 4096 characters. |
body.media_url | string | For media | Publicly reachable URL ReplyBox fetches the media from. Max 100 MB. |
body.caption | string | No | Caption for media messages. Max 4096 characters. |
The channel must belong to your organization and be connected; otherwise the request returns 409 Conflict.
curl -X POST https://api.replybox.utopiagroup.com.my/api/v1/public/messages \
-H "Authorization: Bearer rb_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"channelId": "b1c2d3e4-0000-0000-0000-000000000000",
"to": "60123456789",
"message_type": "text",
"body": { "text": "Hi! Thanks for reaching out." }
}'
curl -X POST https://api.replybox.utopiagroup.com.my/api/v1/public/messages \
-H "Authorization: Bearer rb_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"channelId": "b1c2d3e4-0000-0000-0000-000000000000",
"to": "60123456789",
"message_type": "image",
"body": {
"media_url": "https://example.com/promo.jpg",
"caption": "This week only 🎉"
}
}'
{
"ok": true,
"queued_id": "9a8b7c6d-0000-0000-0000-000000000000",
"status": "pending"
}
The message is queued for delivery. queued_id is the ReplyBox message ID. To learn when it's actually sent to WhatsApp — and when it's delivered, read, or fails — subscribe to the message-update webhook.
This is a separate publishing workflow. A ReplyBox channel is the connected phone number that sends. A WhatsApp Channel is the one-way publishing destination whose JID ends in @newsletter. It is not a contact, group, or ReplyBox inbox conversation.
@newsletter.
Security risk. An API token is as sensitive as a password. Anyone who has it can use the API permissions granted to that token, including reading workspace data and sending messages. Keep it in an environment variable or secrets store. Never paste it into source code, a prompt transcript, a ticket, a screenshot, a log, or a file that Git can commit. Add local secret files such as .env and .env.local to .gitignore before giving a coding agent access to the project.
Only a connected ReplyBox channel with a Baileys connector leg can publish to a WhatsApp Channel. ReplyBox does not list every WhatsApp Channel on the account, show the account's admin role, accept an invitation, or follow a Channel. You must start with the intended WhatsApp Channel invite link or known @newsletter JID.
/public/channel-targets/resolve{
"channelId": "123e4567-e89b-42d3-a456-426614174000",
"target": "https://whatsapp.com/channel/EXAMPLE123"
}
ReplyBox returns the WhatsApp Channel name and safe JID. This lookup does not save, follow, or post to the destination.
{
"channelId": "123e4567-e89b-42d3-a456-426614174000",
"sendingChannelName": "Sales WhatsApp",
"newsletterJid": "120363000000000001@newsletter",
"displayName": "Company Updates"
}
channelId
Resolved WhatsApp Channel name and newsletterJid
The action: save this destination for future publishing
After the human confirms the displayed values, call:
/public/channel-targets{
"channelId": "123e4567-e89b-42d3-a456-426614174000",
"target": "https://whatsapp.com/channel/EXAMPLE123",
"confirm": true
}
ReplyBox resolves the target again and saves the provider result. It does not trust a caller-supplied Channel name or JID. Use GET /public/channel-targets to list the saved destinations available to the token's workspace.
Before every post, show the human the ReplyBox sending channel, WhatsApp Channel name, and complete final text. Call the endpoint only after the human explicitly approves that exact post.
/public/channel-targets/{targetId}/postsIdempotency-Key: your-unique-post-id
{
"text": "The quarterly update is now available: https://example.com/update",
"confirm": true
}
Idempotency-Key is required. The response starts at queued. Reuse the same key only when retrying the exact same target and text. Reusing it with different text returns 409 Conflict instead of risking a second or changed post.
Check the outcome with:
/public/channel-targets/posts/{postId}The result reports queued, sent, or failed, plus a safe error detail when available. A sent result means WhatsApp accepted the post and returned a provider message ID; it does not promise subscriber delivery or read analytics.
Connect this project to ReplyBox so I can publish text to one known WhatsApp Channel.
Before changing code:
1. Ask me for the ReplyBox API base URL, the ReplyBox sending channel ID, and the intended WhatsApp Channel invite link or @newsletter JID.
2. Tell me to store the API token in a local environment variable. Never print it, log it, put it in source code, or commit it. Check that .env and .env.local are in .gitignore.
3. Resolve the destination through POST /public/channel-targets/resolve. Show me the ReplyBox sending channel ID, resolved WhatsApp Channel name, and newsletter JID. Wait for my confirmation.
4. Only after I confirm, save it through POST /public/channel-targets with confirm=true.
5. Before every post, show me the sending channel, destination name, and complete final text. Wait for my explicit approval of that exact post.
6. Publish through POST /public/channel-targets/{targetId}/posts with confirm=true and a unique Idempotency-Key. Then check GET /public/channel-targets/posts/{postId} and report the final status.
Do not discover other Channels, accept or follow an invite, send media, or make a live post without my explicit confirmation.
/public/contactsReturn your organization's contacts (newest first), each with its full set of tags. Supports filtering by tag and searching by name or number.
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 50 | Page size, 1–200. |
offset | integer | 0 | Number of rows to skip. |
tag | string or string[] | — | Return only contacts carrying this tag. Accepts a tag ID or a tag name (case-insensitive). Repeat tag or pass comma-separated values for multi-tag filters. |
tag_match | string | all | With multiple tag values, all requires every tag; any returns contacts carrying at least one. |
q | string | — | Case-insensitive substring search over name, phone, and JID. |
curl "https://api.replybox.utopiagroup.com.my/api/v1/public/contacts?tag=VIP&limit=50" \
-H "Authorization: Bearer rb_live_xxx"
curl "https://api.replybox.utopiagroup.com.my/api/v1/public/contacts?tag=VIP&tag=Lead&tag_match=all" \
-H "Authorization: Bearer rb_live_xxx"
curl "https://api.replybox.utopiagroup.com.my/api/v1/public/contacts?tag=VIP,Lead&tag_match=any" \
-H "Authorization: Bearer rb_live_xxx"
{
"contacts": [
{
"id": "c0ffee00-0000-0000-0000-000000000000",
"wa_jid": "60123456789@s.whatsapp.net",
"phone": "+60123456789",
"display_name": "Aisha Rahman",
"push_name": "Aisha",
"avatar_url": "/api/v1/.../avatar?v=1717000000",
"tags": [
{ "id": "7a…", "name": "VIP", "color_hex": "#2563eb" },
{ "id": "9b…", "name": "Lead", "color_hex": "#16a34a" }
],
"created_at": "2026-06-01T09:15:00+00:00"
}
],
"total": 1,
"limit": 50,
"offset": 0
}
total is the count for the current filter, so you can paginate with offset until offset + len(contacts) >= total.
/public/contacts/{phone}Fetch a single contact by phone number (or JID), with its full tag set.
| Parameter | Type | Description |
|---|---|---|
phone | string | Phone number (digits only) or WhatsApp JID. |
curl "https://api.replybox.utopiagroup.com.my/api/v1/public/contacts/60123456789" \
-H "Authorization: Bearer rb_live_xxx"
{
"id": "c0ffee00-0000-0000-0000-000000000000",
"wa_jid": "60123456789@s.whatsapp.net",
"phone": "+60123456789",
"display_name": "Aisha Rahman",
"push_name": "Aisha",
"avatar_url": null,
"tags": [
{ "id": "7a…", "name": "VIP", "color_hex": "#2563eb" }
],
"created_at": "2026-06-01T09:15:00+00:00"
}
Returns 404 Not Found if no contact with that number exists in your organization.
curl "https://api.replybox.utopiagroup.com.my/api/v1/public/tags" \
-H "Authorization: Bearer rb_live_xxx"
[
{ "id": "7a…", "name": "VIP", "color_hex": "#2563eb" },
{ "id": "9b…", "name": "Lead", "color_hex": "#16a34a" }
]
ReplyBox → You
A webhook is an HTTPS endpoint you host. When a subscribed event occurs, ReplyBox sends it an HTTP POST with a JSON body, signed so you can verify it came from ReplyBox.
Webhooks are managed with the same bearer token as the REST API.
/public/webhooksSubscribe an HTTPS endpoint you host to one or more events.
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | Your HTTPS endpoint. Must start with https://. |
events | string[] | No | Events to subscribe to. Defaults to ["message.received"]. See Events. |
channel_scope | string | No | all (default) — every channel; or specific — only channel_ids. |
channel_ids | string[] | If specific | Channel IDs this subscription applies to. |
secret | string | No | Your own signing secret. If omitted, ReplyBox generates one and returns it once. |
channel_scope applies to message events (message-insert, message-update and their legacy equivalents). Tag events (contact-tag-update / contact.tagged) are organization-wide and always fire regardless of scope.
curl -X POST https://api.replybox.utopiagroup.com.my/api/v1/public/webhooks \
-H "Authorization: Bearer rb_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.example.com/replybox",
"events": ["message-insert", "message-update", "contact-tag-update"],
"channel_scope": "all"
}'
{
"id": "d1e2f3a4-0000-0000-0000-000000000000",
"url": "https://hooks.example.com/replybox",
"events": ["message-insert", "message-update", "contact-tag-update"],
"channel_scope": "all",
"channel_ids": null,
"active": true,
"created_at": "2026-07-07T10:00:00+00:00",
"secret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
Save the secret. It's returned only at creation and is required to verify signatures. Listing webhooks never echoes it.
[
{
"id": "d1e2f3a4-0000-0000-0000-000000000000",
"url": "https://hooks.example.com/replybox",
"events": ["message.received"],
"channel_scope": "all",
"channel_ids": null,
"active": true,
"created_at": "2026-07-07T10:00:00+00:00"
}
]
/public/webhooks/{webhook_id}Partially updates an existing webhook. Secrets are not rotated here; create a new webhook if you need a new signing secret.
| Field | Type | Description |
|---|---|---|
url | string | New HTTPS endpoint URL. |
events | string[] | New subscribed event list. |
channel_scope | string | all or specific. |
channel_ids | string[] | Required when the effective channel_scope is specific. |
active | boolean | Enable or disable delivery without deleting the subscription. |
curl -X PATCH https://api.replybox.utopiagroup.com.my/api/v1/public/webhooks/d1e2f3a4-0000-0000-0000-000000000000 \
-H "Authorization: Bearer rb_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.example.com/replybox-v2",
"events": ["message-insert", "message-update"],
"active": true
}'
{
"id": "d1e2f3a4-0000-0000-0000-000000000000",
"url": "https://hooks.example.com/replybox-v2",
"events": ["message-insert", "message-update"],
"channel_scope": "all",
"channel_ids": null,
"active": true,
"created_at": "2026-07-07T10:00:00+00:00"
}
/public/webhooks/{webhook_id}Returns 204 No Content on success, or 404 Not Found if the webhook isn't in your organization.
Every delivery is a POST with these headers:
| Header | Description |
|---|---|
X-Replybox-Event | The event name, e.g. message-insert. |
X-Replybox-Event-Id | A stable ID for the event — use it to deduplicate. |
X-Replybox-Timestamp | Unix seconds when the request was signed. |
X-Replybox-Signature | sha256=<hex> HMAC signature — see Verifying signatures. |
Respond with any 2xx status to acknowledge. Non-2xx responses are retried (see Delivery, retries & idempotency).
ReplyBox offers two event formats. Use the v2 events below for new integrations — they use a uniform envelope and cover the full message lifecycle. The legacy events are still delivered for existing consumers.
Every v2 event has the same top-level shape. The event field tells you what happened; the details are always under data:
{
"event": "message-insert",
"timestamp": "2026-07-07T10:05:00+00:00",
"data": { "...": "event-specific fields" }
}
message-insertAn inbound WhatsApp message arrives (groups included).
message-updateAn outbound message changes status: sent, delivered, read, or failed.
contact-tag-updateA contact's tags change (added or removed).
message-insert{
"event": "message-insert",
"timestamp": "2026-07-07T10:05:00+00:00",
"data": {
"id": "3EB0…",
"channel_id": "b1c2d3e4-…",
"conversation_id": "11112222-…",
"direction": "inbound",
"type": "text",
"text": "Is this available?",
"is_group": false,
"group_jid": null,
"contact": {
"wa_jid": "60123456789@s.whatsapp.net",
"phone": "+60123456789",
"name": "Aisha"
},
"sender_jid": "60123456789@s.whatsapp.net",
"mentioned_jids": [],
"reply_to_message_id": null,
"quoted_text": null,
"created_at": "2026-07-07T10:05:00+00:00"
}
}
message-updateFires on every status transition of an outbound message, so you can track the full delivery lifecycle. Use data.id (the WhatsApp message ID) to correlate the transitions of a single message.
data.status is one of sent, delivered, read, or failed.
{
"event": "message-update",
"timestamp": "2026-07-07T10:06:03+00:00",
"data": {
"id": "3EB0…",
"channel_id": "b1c2d3e4-…",
"conversation_id": "11112222-…",
"direction": "outbound",
"type": "text",
"text": "Hi! Thanks for reaching out.",
"status": "delivered",
"contact": {
"wa_jid": "60123456789@s.whatsapp.net",
"phone": "+60123456789",
"name": "Aisha Rahman"
},
"created_at": "2026-07-07T10:06:00+00:00"
}
}
contact-tag-updateFires whenever a contact's tags change. data.tags is the contact's complete current tag set, so you can treat each event as the authoritative state and mirror removals as well as additions.
{
"event": "contact-tag-update",
"timestamp": "2026-07-07T10:07:00+00:00",
"data": {
"contact": {
"id": "c0ffee00-…",
"wa_jid": "60123456789@s.whatsapp.net",
"phone": "+60123456789",
"name": "Aisha Rahman"
},
"tags": [
{ "id": "7a…", "name": "VIP", "color_hex": "#2563eb" },
{ "id": "9b…", "name": "Lead", "color_hex": "#16a34a" }
],
"tag_ids": ["7a…", "9b…"]
}
}
When the last tag is removed, tags and tag_ids are empty arrays.
These flat events (no data envelope) predate the v2 catalog and remain supported for backward compatibility. New integrations should prefer the v2 events above. Subscribe to a legacy event by name and you'll receive the legacy shape; the two coexist on the same webhook.
| Legacy event | v2 equivalent |
|---|---|
message.received | message-insert |
message.sent (fires once, on sent only) | message-update (fires on every status) |
contact.tagged | contact-tag-update |
message.received{
"event": "message.received",
"channel_id": "b1c2d3e4-…",
"org_id": "a0a0a0a0-…",
"conversation_id": "11112222-…",
"group_jid": null,
"is_group": false,
"provider_message_id": "3EB0…",
"direction": "inbound",
"sender_jid": "60123456789@s.whatsapp.net",
"sender_name": "Aisha",
"kind": "text",
"body": "Is this available?",
"mentioned_jids": [],
"reply_to_message_id": null,
"quoted_text": null,
"timestamp": "2026-07-07T10:05:00+00:00"
}
message.sent & contact.tagged{
"event": "message.sent",
"channel_id": "b1c2d3e4-…",
"org_id": "a0a0a0a0-…",
"conversation_id": "11112222-…",
"provider_message_id": "3EB0…",
"direction": "outbound",
"recipient_jid": "60123456789@s.whatsapp.net",
"recipient_phone": "+60123456789",
"recipient_name": "Aisha Rahman",
"kind": "text",
"body": "Hi! Thanks for reaching out.",
"status": "sent",
"timestamp": "2026-07-07T10:06:00+00:00"
}
{
"event": "contact.tagged",
"org_id": "a0a0a0a0-…",
"contact_id": "c0ffee00-…",
"wa_jid": "60123456789@s.whatsapp.net",
"phone": "+60123456789",
"display_name": "Aisha Rahman",
"tags": [
{ "id": "7a…", "name": "VIP", "color_hex": "#2563eb" }
],
"tag_ids": ["7a…"],
"timestamp": "2026-07-07T10:07:00+00:00"
}
Every request is signed with the webhook's secret using HMAC-SHA256. Always verify the signature before trusting a payload.
The signed string is the request timestamp and the raw request body joined with a dot:
signed_string = "{X-Replybox-Timestamp}.{raw_request_body}"
signature = hex( HMAC_SHA256(secret, signed_string) )
Compare that against the X-Replybox-Signature header (drop the sha256= prefix) using a constant-time comparison.
Verify against the raw body bytes, before any JSON re-serialization — re-encoding can change whitespace and break the signature.
const crypto = require("crypto");
// Use the raw body: app.use(express.raw({ type: "application/json" }))
function verifyReplyBox(req, secret) {
const ts = req.header("X-Replybox-Timestamp");
const received = (req.header("X-Replybox-Signature") || "").replace("sha256=", "");
const expected = crypto
.createHmac("sha256", secret)
.update(`${ts}.${req.body}`) // req.body is the raw Buffer/string
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
}
app.post("/replybox", express.raw({ type: "application/json" }), (req, res) => {
if (!verifyReplyBox(req, process.env.REPLYBOX_WEBHOOK_SECRET)) {
return res.status(401).send("bad signature");
}
const event = JSON.parse(req.body);
// ... handle event ...
res.sendStatus(200);
});
import hmac, hashlib
from flask import request, abort
def verify_replybox(secret: str) -> bytes:
ts = request.headers.get("X-Replybox-Timestamp", "")
received = request.headers.get("X-Replybox-Signature", "").removeprefix("sha256=")
raw = request.get_data() # raw bytes, before JSON parsing
expected = hmac.new(
secret.encode(), f"{ts}.".encode() + raw, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(received, expected):
abort(401)
return raw
A delivery that doesn't return 2xx (or times out after 5 s) is retried up to 3 times total, with backoff (immediately, then ~5 s, then ~30 s). After that it's dropped.
Retries reuse the same X-Replybox-Event-Id. Because a timeout can occur after your server processed the event, treat handlers as idempotent — dedupe on X-Replybox-Event-Id.
Events are dispatched independently and are not guaranteed to arrive in order. Use the timestamp field (and, for messages, the current tag set / status) rather than assuming arrival order.
Only public HTTPS destinations are accepted; requests to private, loopback, or internal addresses are rejected.
Optionally reject deliveries whose X-Replybox-Timestamp is more than a few minutes old to mitigate replay.
Media delivery
The message media endpoint (message media GET) honours Range headers and returns Accept-Ranges / Content-Range as appropriate (200 / 206 / 416). This enables seeking and scrubbing on voice notes.
Clients that probe media length or implement seek should use Range and honor 206 responses.
HEAD requests are served by the same handler and return the same length and accept-ranges headers as a GET probe. Previous behaviour returned 405 on HEAD for some clients.
Auth, tenancy, decryption, and cache policy remain unchanged.
Connector → ReplyBox
channel_heartbeat: post-boot grace and deferredstarted_at (ISO-8601 Z) may be included in the connector heartbeat to indicate the connector process start time. There is no behaviour change if started_at is absent, so the field is backwards-compatible.
Within CONNECTOR_BOOT_DEMOTION_GRACE_SECONDS (default 300s) after started_at, channels the connector omits that look like orphans are logged and returned as deferred instead of being demoted. Real demotion happens after the window.
The response adds deferred: <number>. Older connectors and APIs ignore the key and read it as 0.
started_at must be a parseable timezone-aware string or the request is 400. Small future skew counts as just-booted; absurd far-future times are ignored so demotion cannot be suppressed forever.
Read-only boards
Staff-only read access: any logged-in staff member can read both boards.
/api/v1/rankings/salesPassthrough view of the canonical leaderboard. ReplyBox does not recompute sales figures; numbers must match the leaderboard app.
/api/v1/rankings/missed-callsCombined view of WhatsApp (DB aggregate) and phone (call-tracker feed). The endpoint ranks by rate, not raw count.
Upstream payloads are cached in-process and served as the “last good copy” with stale: true. If no cached copy exists and the upstream is unavailable, the route returns 502.
The phone and WhatsApp legs degrade independently. One leg down still serves the other leg's data, with the missing leg reported as not configured / ok:false; only both legs down yields 502.
Allowed period values are today, current_week, last_week, and mtd. Unknown periods are rejected by the route (422).
Missed-call aggregation never returns caller identifiers (phone numbers, JIDs, or message text). Only aggregated counts and line labels are exposed.
Five steps
Dashboard → Settings → Developers → API Token → Generate.
Dashboard → Channels (or list via the app). You'll pass it as channelId when sending.
curl -X POST https://api.replybox.utopiagroup.com.my/api/v1/public/messages \
-H "Authorization: Bearer $REPLYBOX_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "channelId": "<CHANNEL_ID>", "to": "60123456789",
"message_type": "text", "body": { "text": "Hello from the API!" } }'
Stand up an HTTPS endpoint, then register it:
curl -X POST https://api.replybox.utopiagroup.com.my/api/v1/public/webhooks \
-H "Authorization: Bearer $REPLYBOX_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "url": "https://hooks.example.com/replybox",
"events": ["message-insert", "message-update", "contact-tag-update"] }'
On every incoming webhook, using the returned secret (see Verifying signatures).
Support
Contact support@utopiagroup.com.my (or your ReplyBox account manager). Include your organization name and, for webhook problems, the X-Replybox-Event-Id of an affected delivery.
ReplyBox is operated by UTOPIA ACCELERATOR SDN BHD.