> This page is for version v2 (default).
> For other versions, use one of these documentation indexes:
> - v2 (default): https://docs.pinnacle.sh/v-2/llms.txt
> - v1: https://docs.pinnacle.sh/v1/llms.txt

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.pinnacle.sh/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.pinnacle.sh/_mcp/server.

# Receiving Message, Call, and User Events

> Process and validate incoming webhook requests

When your webhook receives inbound messages, call status updates, user events, form submissions, or campaign status updates from Pinnacle, the raw request body lacks type information. The `process` method validates incoming events by checking the signing secret and transforms the request into a fully typed [`MessageEvent`](/webhooks/message-events), [`CallStatusEvent`](/webhooks/call-events), [`UserEvent`](/webhooks/user-events), [`FormSubmissionEvent`](/webhooks/form-events), or [`CampaignStatusEvent`](/webhooks/campaign-events) object.

## Parameters

#### TypeScript

| Parameter | Type                            | Description                                                                                                                                          |
| --------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `req`     | `Request \| ExpressLikeRequest` | Standard Fetch API `Request` object or Express-like request that must contain both a `headers` and `body` field.                                     |
| `secret`  | `string` (optional)             | Signing secret for webhook validation. If not provided, uses `PINNACLE_SIGNING_SECRET` environment variable. Throws an error if neither is provided. |

#### Python

| Parameter | Type              | Description                                                                                                            |
| --------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `req`     | `PinnacleRequest` | `dict` with:   - `headers`: `Dict[str, Any]`   - `body`: `str` (JSON string)                                           |
| `secret`  | `str` (optional)  | Signing secret for webhook validation. If not provided, uses `PINNACLE_SIGNING_SECRET`. Raises if neither is provided. |

#### Ruby

| Parameter | Type                | Description                                                                                                                    |
| --------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `request` | `Hash`              | Hash with:   - `:headers`: `Hash` containing request headers   - `:body`: `String` (JSON string) or `Hash` (parsed JSON)       |
| `secret:` | `String` (optional) | Signing secret for webhook validation. If not provided, uses `PINNACLE_SIGNING_SECRET` env var. Raises if neither is provided. |

## Returns

One of the following objects (discriminated by the `type` field):

* [`MessageEvent`](/webhooks/message-events): Inbound message or message status update (`MESSAGE.RECEIVED`, `MESSAGE.STATUS`).
* [`UserEvent`](/webhooks/user-events): User event such as when a user started typing (`USER.TYPING`).
* [`FormSubmissionEvent`](/webhooks/form-events): A recipient completed a hosted form (`FORM.SUBMISSION`).
* [`CampaignStatusEvent`](/webhooks/campaign-events): Per-carrier launch or verification status changed for an RCS campaign (`CAMPAIGN.STATUS`).
* [`CallStatusEvent`](/webhooks/call-events): Voice call lifecycle status update (`CALL.STATUS`).

## Errors

| Error               | Description                      |
| ------------------- | -------------------------------- |
| `UnauthorizedError` | Webhook secret validation failed |
| `BadRequestError`   | Request body validation failed   |

## Example Implementation

#### TypeScript

```typescript
import express from 'express';
import { PinnacleClient, Pinnacle } from 'rcs-js';

const app = express();
const client = new PinnacleClient({ apiKey: PINNACLE_API_KEY });

app.post('/webhook', express.json(), async (req, res) => {
  try {
    // Process and validate the webhook. Returns one of:
    //   Pinnacle.MessageEvent | Pinnacle.CallStatusEvent | Pinnacle.UserEvent | Pinnacle.FormSubmissionEvent | Pinnacle.CampaignStatusEvent
    const event = await client.messages.process(req);

    switch (event.type) {
      case "MESSAGE.RECEIVED":
      case "MESSAGE.STATUS":
        console.log(`${event.direction} ${event.type}:`, event.message);
        break;
      case "CALL.STATUS":
        console.log("call", event.call.id, "status:", event.call.status);
        break;
      case "USER.TYPING":
        console.log("user typing in conversation", event.conversation.id);
        break;
      case "FORM.SUBMISSION":
        console.log("form", event.form.id, "submitted:", event.submission.data);
        break;
      case "CAMPAIGN.STATUS":
        // Per-carrier launch + verification status for an RCS campaign.
        console.log(
          "campaign status for", event.agent.id,
          "carriers:", event.carrierLaunches.carriers,
          "verification:", event.carrierLaunches.verification,
        );
        break;
    }

    res.status(200).json({ status: 'processed' });
  } catch (error) {
      throw error
  }
});

```

#### Python

```python
from fastapi import FastAPI, Request, HTTPException
from rcs import (
    Pinnacle,
    MessageEvent,
    CallStatusEvent,
    UserEvent,
    FormSubmissionEvent,
    CampaignStatusEvent,
)
import os
from dotenv import load_dotenv

load_dotenv()

app = FastAPI(title="RCS Webhook Server")

# Initialize Pinnacle client
client = Pinnacle(api_key=os.environ.get("PINNACLE_API_KEY"))

@app.post("/webhook")
async def webhook_handler(request: Request):
    """Handle incoming RCS webhooks."""
    try:
        # Process and validate the webhook. Returns a fully typed event.
        event: (
            MessageEvent | CallStatusEvent | UserEvent | FormSubmissionEvent | CampaignStatusEvent
        ) = client.messages.process(
            {
                "headers": dict(request.headers),
                "body": (await request.body()).decode("utf-8"),
            }
        )

        if isinstance(event, CallStatusEvent):
            print(f"call {event.call.id} status: {event.call.status}")
        elif isinstance(event, FormSubmissionEvent):
            print(f"form {event.form.id} submitted: {event.submission.data}")
        elif isinstance(event, CampaignStatusEvent):
            # Per-carrier launch + verification status for an RCS campaign.
            print(
                f"campaign status for {event.agent.id} -- "
                f"carriers: {event.carrier_launches.carriers}, "
                f"verification: {event.carrier_launches.verification}"
            )
        elif isinstance(event, UserEvent):
            print(f"user typing in conversation {event.conversation.id}")
        else:
            # MessageEvent — MESSAGE.RECEIVED or MESSAGE.STATUS
            print(f"{event.direction} {event.type}: {event.message}")

        return {"status": "success", "event_type": type(event).__name__}

    except Exception as error:
        raise HTTPException(status_code=400, detail=str(error))

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)
```

#### Ruby

```ruby
require 'sinatra'
require 'rcs'

# Initialize Pinnacle client
client = Pinnacle::Client.new(api_key: ENV['PINNACLE_API_KEY'])

post '/webhook' do
  content_type :json

  begin
    # Process and validate the webhook. Returns a fully typed event:
    #   Pinnacle::Types::MessageEvent | CallStatusEvent | UserEvent | FormSubmissionEvent | CampaignStatusEvent
    event = client.messages.process(
      {
        headers: request.env.select { |k, _| k.start_with?('HTTP_') }
                      .transform_keys { |k| k.sub('HTTP_', '').split('_').map(&:capitalize).join('-') },
        body: request.body.read
      }
    )

    case event
    when Pinnacle::Types::CallStatusEvent
      puts "call #{event.call.id} status: #{event.call.status}"
    when Pinnacle::Types::FormSubmissionEvent
      puts "form #{event.form.id} submitted: #{event.submission.data}"
    when Pinnacle::Types::CampaignStatusEvent
      # Per-carrier launch + verification status for an RCS campaign.
      puts "campaign status for #{event.agent.id} -- " \
           "carriers: #{event.carrier_launches.carriers}, " \
           "verification: #{event.carrier_launches.verification}"
    when Pinnacle::Types::UserEvent
      puts "user typing in conversation #{event.conversation.id}"
    else
      # MessageEvent — MESSAGE.RECEIVED or MESSAGE.STATUS
      puts "#{event.direction} #{event.type}: #{event.message}"
    end

    { status: 'success', event_type: event.class.name }.to_json

  rescue Pinnacle::Errors::UnauthorizedError => e
    status 401
    { error: e.message }.to_json
  rescue Pinnacle::Errors::ClientError => e
    status 400
    { error: e.message }.to_json
  end
end
```