Forwarding messages via webhook

RCS Studio can forward real-time events to your server — inbound user messages, delivery receipts, and connector activity. Configure a webhook URL on your agent and RCS Studio will POST each event to that endpoint as it occurs.

Event overview

The diagram below shows a typical exchange and when each event fires relative to the conversation.

sequenceDiagram
    actor User
    participant R as RCS Studio
    participant S as Your server

    User->>R: Sends text or taps a chip
    R->>User: Agent reply
    R->>S: UserMessage event
    Note right of S: X-Vibes-Response header present<br/>because the agent replied

    alt Delivery confirmed
        R->>S: UserEvent (DELIVERED)
    else Delivery fails
        R->>S: ServerEvent (FAILED)
    end

    User->>R: Taps another chip
    R->>User: Agent reply
    R->>S: UserMessage event
    R->>S: UserEvent (DELIVERED)

Three event classes:

Event classWhen it firesTriggers agent processing
UserMessageUser sends a message, taps a chip, shares location, or uploads a fileYes — the agent resolves state and sends a reply. X-Vibes-Response is set if a reply was sent.
UserEventUser-side events — delivery confirmation (DELIVERED), read receipt (READ), typing indicator (IS_TYPING), and subscribe/unsubscribeNo — forwarded as-is
ServerEventServer-side outcome of an outbound message — SENT, FAILED, and TTL eventsNo — forwarded as-is

Setting up a webhook

Set the Event forwarding URL on your agent. You can do this in the RCS Studio UI under your agent's Settings tab, or via the API:

curl -X PATCH https://api.rcsstudio.ai/agents/ag_abc123 \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"config": {"eventForwardUrl": "https://your-server.com/webhook"}}'

Your endpoint must be publicly reachable over HTTPS and return a 2xx response. Events that fail to deliver are retried.


Request format

RCS Studio sends an HTTP POST to your endpoint for each event:

POST https://your-server.com/webhook HTTP/1.1
Host: your-server.com
User-Agent: rcs-studio/1.0
Content-Type: application/json
X-Vibes-Eventclass: UserMessage
X-Vibes-Signature: <base64-encoded HMAC-SHA512 of the request body>
X-Vibes-Response: <state ID or keyword, only present on UserMessage events when the agent replied>

{ ...event payload... }

Request headers

HeaderDescription
X-Vibes-EventclassThe event class — UserMessage, UserEvent, or ServerEvent.
X-Vibes-SignatureHMAC-SHA512 signature of the raw request body, base64-encoded. See Verifying signatures.
X-Vibes-ResponseThe state ID (or keyword) of the message your agent sent in response to this event. Only present on UserMessage events when the agent triggered a reply. Use this to correlate an inbound user event with the outbound message it produced.

UserMessage events

UserMessage events fire when a user interacts with your agent. These are the only events that trigger agent processing — RCS Studio resolves the next state and sends a reply before forwarding the event to your webhook.

The payload schema varies by the type of user action. All variants share a common set of fields; additional fields are present depending on what the user did.

Common fields

FieldTypeDescription
agentIdstringThe agent that received the message
senderPhoneNumberstringThe user's phone number
messageIdstringUnique identifier for this user message
sendTimestringISO 8601 timestamp of when the message was sent
richMessageClassificationobjectMessage classification — classificationType (RICH_MESSAGE or RICH_MEDIA_MESSAGE) and segmentCount

Variant fields

FieldTypePresent when
textstringUser sent a text message
suggestionResponse.textstringUser tapped a chip — the chip's display label
suggestionResponse.postbackDatastringUser tapped a chip — the postback data set on the chip
suggestionResponse.typestringUser tapped a chip — "REPLY" for suggested reply chips, "ACTION" for action chips
location.latitudestringUser shared their location
location.longitudestringUser shared their location
userFile.payload.mimeTypestringUser uploaded a file
userFile.payload.fileSizeBytesnumberUser uploaded a file
userFile.payload.fileNamestringUser uploaded a file
userFile.payload.fileUristringUser uploaded a file — URL to the file content
userFile.thumbnailobjectUser uploaded an image or video — contains mimeType, fileSizeBytes, and fileUri

Example — text message

POST /webhook HTTP/1.1
X-Vibes-Eventclass: UserMessage
X-Vibes-Signature: <signature>
X-Vibes-Response: order-status

{
  "agentId": "example_agent",
  "senderPhoneNumber": "+15551234567",
  "messageId": "MxZIMfKVnURVm7GEMvpbaIng",
  "sendTime": "2026-04-28T14:30:00.000000Z",
  "text": "Check my order",
  "richMessageClassification": {
    "classificationType": "RICH_MESSAGE",
    "segmentCount": 1
  }
}

Example — chip tap

POST /webhook HTTP/1.1
X-Vibes-Eventclass: UserMessage
X-Vibes-Signature: <signature>
X-Vibes-Response: order-status

{
  "agentId": "example_agent",
  "senderPhoneNumber": "+15551234567",
  "messageId": "66ecf984-5698-4256-963a-f2048c90b7fe",
  "sendTime": "2026-04-28T14:30:00.000000Z",
  "richMessageClassification": {
    "classificationType": "RICH_MESSAGE",
    "segmentCount": 1
  },
  "suggestionResponse": {
    "postbackData": "check_order",
    "text": "Check my order",
    "type": "REPLY"
  }
}

Example — location share

POST /webhook HTTP/1.1
X-Vibes-Eventclass: UserMessage
X-Vibes-Signature: <signature>

{
  "agentId": "example_agent",
  "senderPhoneNumber": "+15551234567",
  "messageId": "3d184cb5-c4be-475c-9c20-04bc5240014a",
  "sendTime": "2026-04-28T14:30:00.000000Z",
  "richMessageClassification": {
    "classificationType": "RICH_MESSAGE",
    "segmentCount": 1
  },
  "location": {
    "latitude": "37.7749",
    "longitude": "-122.4194"
  }
}

Example — file share

POST /webhook HTTP/1.1
X-Vibes-Eventclass: UserMessage
X-Vibes-Signature: <signature>

{
  "agentId": "example_agent",
  "senderPhoneNumber": "+15551234567",
  "messageId": "Mx1cxm-SCFRtuiHK8mqJw0Nw",
  "sendTime": "2026-04-28T14:30:00.000000Z",
  "richMessageClassification": {
    "classificationType": "RICH_MEDIA_MESSAGE",
    "segmentCount": 1
  },
  "userFile": {
    "payload": {
      "mimeType": "image/jpeg",
      "fileSizeBytes": 204800,
      "fileName": "photo.jpg",
      "fileUri": "https://storage.googleapis.com/rbm-uploads/..."
    },
    "thumbnail": {
      "mimeType": "image/jpeg",
      "fileSizeBytes": 4096,
      "fileUri": "https://storage.googleapis.com/rbm-uploads/..."
    }
  }
}

UserEvent events

UserEvent events are fired by the user's device — delivery confirmations, read receipts, typing indicators, and subscribe/unsubscribe notifications. They do not trigger any agent processing; RCS Studio forwards them to your webhook unchanged.

Fields

FieldTypeDescription
agentIdstringThe agent associated with this event
senderPhoneNumberstringThe user's phone number
messageIdstringThe ID of the message this event relates to
eventTypestringOne of: DELIVERED, READ, IS_TYPING, SUBSCRIBE, UNSUBSCRIBE
eventIdstringUnique identifier for this event
sendTimestringISO 8601 timestamp

Example — DELIVERED

POST /webhook HTTP/1.1
X-Vibes-Eventclass: UserEvent
X-Vibes-Signature: <signature>

{
  "agentId": "example_agent",
  "sendTime": "2026-05-18T00:12:42.000000Z",
  "senderPhoneNumber": "+15551234567",
  "eventType": "DELIVERED",
  "eventId": "MxkiHGGOfhSvSi3xIsj-26MQ",
  "messageId": "be0ed04a-ff3c-4a1a-8c64-9235fb4f9073"
}

ServerEvent events

ServerEvent events report the server-side outcome of an outbound agent message. They do not trigger any agent processing; RCS Studio forwards them to your webhook unchanged.

📘

ServerEvent payloads use phoneNumber (not senderPhoneNumber) for the recipient's number.

Fields

FieldTypeDescription
agentIdstringThe agent that sent the message
phoneNumberstringThe recipient's phone number
messageIdstringThe ID of the outbound message
eventTypestringOne of: SENT, FAILED, TTL_EXPIRATION_REVOKED, TTL_EXPIRATION_REVOKE_FAILED
eventIdstringUnique identifier for this event
eventOriginstringSource of the event — VIBES or CARRIER
sendTimestringISO 8601 timestamp
richMessageClassificationobject(SENT only) Classification of the sent message
errorCodestring(FAILED, conditional) HTTP status code returned by the RCS platform, as a string
errorMessagestring(FAILED, conditional) Raw HTTP response body from the RCS platform

Example — SENT

POST /webhook HTTP/1.1
X-Vibes-Eventclass: ServerEvent
X-Vibes-Signature: <signature>

{
  "agentId": "example_agent",
  "sendTime": "2026-05-18T00:12:42.000000Z",
  "phoneNumber": "+15551234567",
  "messageId": "be0ed04a-ff3c-4a1a-8c64-9235fb4f9073",
  "eventType": "SENT",
  "eventId": "75078f52-5ed0-4d95-95d8-0cb5a7c7dede",
  "eventOrigin": "VIBES",
  "richMessageClassification": {
    "classificationType": "RICH_MESSAGE",
    "segmentCount": 1
  }
}

FAILED

Fired when an agent message cannot be delivered. The errorCode and errorMessage fields are present when the failure occurred at the RCS platform level — for example, the number is not an RCS subscriber. They are absent when the failure was detected before the send attempt was made.

Example — platform rejection

The most common failure case. The send reached the RCS platform but was rejected — for example, because the destination number is not an RCS subscriber.

POST /webhook HTTP/1.1
X-Vibes-Eventclass: ServerEvent
X-Vibes-Signature: <signature>

{
  "agentId": "example_agent",
  "phoneNumber": "+15551234567",
  "messageId": "f344c24b-4871-11f1-a4e3-000000000043",
  "eventType": "FAILED",
  "eventId": "de98421c-a54f-4469-b667-9d9e819444d2",
  "eventOrigin": "VIBES",
  "errorCode": "404",
  "errorMessage": "404 Not Found\nPOST https://us-rcsbusinessmessaging.googleapis.com/v1/phones/...",
  "sendTime": "2026-05-18T00:12:42.014957638Z"
}

Example — pre-delivery failure

The failure was detected before the message reached the RCS platform — for example, a capability check determined the device does not support RCS.

POST /webhook HTTP/1.1
X-Vibes-Eventclass: ServerEvent
X-Vibes-Signature: <signature>

{
  "agentId": "example_agent",
  "phoneNumber": "+15551234567",
  "messageId": "f344c24b-4871-11f1-a4e3-000000000043",
  "eventType": "FAILED",
  "eventId": "de98421c-a54f-4469-b667-9d9e819444d2",
  "eventOrigin": "VIBES",
  "sendTime": "2026-05-18T00:12:42.014957638Z"
}

Always treat errorCode and errorMessage as optional — check eventType === "FAILED" first, then inspect the error fields for additional detail:

function handleWebhookEvent(headers, payload) {
  if (headers['X-Vibes-Eventclass'] === 'ServerEvent' && payload.eventType === 'FAILED') {
    const detail = payload.errorCode
      ? `${payload.errorCode} — ${payload.errorMessage}`
      : 'No error detail available';
    console.error(`Message ${payload.messageId} failed: ${detail}`);
  }
}

Verifying signatures

Every webhook request includes an X-Vibes-Signature header with an HMAC-SHA512 signature of the raw request body, encoded as base64.

⚠️

Webhook signing tokens are not yet self-serve. To receive a signing token for your account, contact your Vibes representative. Once issued, the token is tied to your account and applies to all agents under it. Until you have a token, you can still receive and process events — signature verification is optional but strongly recommended for production use.

Once you have your signing token:

  1. Read the raw request body before parsing it as JSON
  2. Compute HMAC-SHA512(body, signingToken) and base64-encode the result
  3. Compare your computed value to the X-Vibes-Signature header value
  4. Reject requests where the signatures do not match — return 403
import crypto from 'crypto';

function verifySignature(rawBody, signature, signingToken) {
  const expected = crypto
    .createHmac('sha512', signingToken)
    .update(rawBody)
    .digest('base64');
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}

HTTP responses

Return a 2xx status code to acknowledge receipt.

📘

4xx responses are treated as permanent failures — the event moves to the failure queue without retrying. Return 5xx if you need a transient failure to trigger a retry.

Acknowledge events quickly and process them asynchronously if the work is time-consuming — long-running handlers risk timing out and triggering unnecessary retries.


See also