> 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 Messages and User Events

> Process and validate incoming webhook requests

When your webhook receives inbound messages, 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), [`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`).

## 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.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 "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,
    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 | UserEvent | FormSubmissionEvent | CampaignStatusEvent
        ) = client.messages.process(
            {
                "headers": dict(request.headers),
                "body": await request.body(),
            }
        )

        if 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 | 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::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
```