> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs.pinnacle.sh/v-2/guides/messages/receiving/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 incoming webhooks from Pinnacle to receive message replies, delivery updates, and user events. ## Setting up webhooks To receive inbound messages and status updates, configure a webhook in the [Pinnacle Dashboard](https://app.pinnacle.sh/dashboard/development/webhooks): #### Create a webhook Navigate to **Development > Webhooks** and click **Create new webhook**. Give it a descriptive name and enter your endpoint URL. For local development, use an [ngrok](https://ngrok.com/) tunnel. #### Save the signing secret After creation, copy the **signing secret** (prefixed `pss_`) and store it in your environment as `PINNACLE_SIGNING_SECRET`. #### Attach senders Attach one or more phone numbers or RCS agent IDs to the webhook so it receives their events. You can attach and detach senders in the dashboard or in bulk via [`POST /webhooks/attach`](/api-reference/webhooks/attach-webhook) and [`POST /webhooks/detach`](/api-reference/webhooks/detach-webhook). > **Sandbox numbers** > > For sandbox numbers, ensure you've whitelisted the recipient device and > verified the 4-digit PIN before testing. ## Custom request headers You can configure additional HTTP headers that Pinnacle includes on every webhook delivery — for example, a static API key, an internal routing token, or a tracing identifier. Set them in the dashboard when creating or editing a webhook, or pass an optional `headers` map to [`POST /webhooks/attach`](/api-reference/webhooks/attach-webhook). Headers can be supplied whether you're creating a new webhook or attaching an existing one by ID: ```json // Creating a new webhook { "name": "Orders webhook", "url": "https://example.com/webhook", "headers": { "X-API-KEY": "sk_live_...", "X-TENANT-ID": "tenant_42" }, "senders": ["+14155551234"] } ``` ```json // Attaching an existing webhook — headers overwrite what's stored { "webhookId": "wh_1234567890", "headers": { "X-API-KEY": "sk_live_rotated_...", "X-TENANT-ID": "tenant_42" }, "senders": ["+14155551234"] } ``` **Header rules:** * Names must match the pattern `^[A-Za-z0-9][A-Za-z0-9_-]*$` — start with a letter or digit, then letters, digits, `-`, or `_`. * Names are case-insensitive per [RFC 9110](https://datatracker.ietf.org/doc/html/rfc9110#name-field-names) and normalized to uppercase before storage and sending. * Values must be strings. * The `PINNACLE-SIGNING-SECRET` header is reserved for signature verification — any attempt to set it is silently stripped. **Overwrite semantics when using `webhookId`:** * Supplying `headers` **replaces** the entire stored header map on that webhook. * Omitting `headers` leaves the stored headers unchanged. * Passing an empty object `{}` clears all custom headers from the webhook. ## Processing webhook events Pinnacle SDKs provide a [`process()`](/methods/process) method to securely handle incoming webhook requests: * Verifies webhook signatures by comparing your signing secret with the `PINNACLE-SIGNING-SECRET` in the headers. * Parses and validates the request payload. * Returns fully typed [`MessageEvent`](/webhooks/message-events) or [`UserEvent`](/webhooks/user-events) objects. ## Event types | Event | Description | | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `MESSAGE.RECEIVED` | Inbound messages and button clicks from users. | | `MESSAGE.STATUS` | Status updates for your sent messages (including `FALLBACK_SENT` when an RCS message fails and a fallback SMS/MMS is sent instead). | | `USER.TYPING` | User started typing (RCS only). | | `FORM.SUBMISSION` | A recipient completed a hosted form sent via [`POST /forms/send`](/api-reference/forms/send-form). | | `CAMPAIGN.STATUS` | Per-carrier launch (AT\&T / T-Mobile / Verizon / Other) or verification (AEGIS / Google) status changed for a campaign. RCS agent senders only. | For full code examples in TypeScript, Python, and Ruby, see the [SMS quickstart](/quickstart/sms) and [RCS quickstart](/quickstart/rcs) receive guides. ## Form submissions When a recipient fills out a form delivered via [`POST /forms/send`](/api-reference/forms/send-form), a `FORM.SUBMISSION` event is delivered to every webhook subscribed to the sender. The payload differs from message events — there is no `message` or `status` field; instead the event carries a resolved snapshot of the form definition paired with the submitted values so you can render or route on the response without an extra `get_form` call. **Payload shape:** ```json { "type": "FORM.SUBMISSION", "sender": "agent_iM9wQcyBBjYn", "conversation": { "id": "convo_H2tiG5kvhxQQHUb6", "from": "agent_iM9wQcyBBjYn", "to": "+14155551234" }, "form": { "id": "form_abc123", "url": "https://forms.pinnacle.sh/form_abc123", "name": "Contact request" }, "submission": { "id": "fsub_xyz789", "from": "agent_iM9wQcyBBjYn", "to": "+14155551234", "data": { "full_name": "Ada Lovelace", "email": "ada@example.com", "plan": "pro", "interests": ["rcs", "sms"] }, "fields": [ { "key": "full_name", "label": "Full name", "type": "text", "value": "Ada Lovelace" }, { "key": "email", "label": "Email", "type": "email", "value": "ada@example.com" }, { "key": "plan", "label": "Plan", "type": "select", "value": "pro" }, { "key": "interests", "label": "Interests", "type": "checkbox", "value": ["rcs", "sms"] } ], "ip_address": "203.0.113.45", "user_agent": "Mozilla/5.0 …", "submitted_at": "2026-04-24T00:35:04.406+00:00" } } ``` **Notes:** * `sender` at the event root is convenience-duplicated from `submission.from` so you can route on it without unwrapping. * `conversation` is `null` when the form was minted URL-only (no `to` at send time). In that case `submission.to` is also `null`. * For `can_update=true` forms, each edit fires a fresh `FORM.SUBMISSION` event with the updated values. * Every field from the form definition is included in `submission.fields`, even those the recipient left blank — check `value` for `null` or an empty array. ## RCS Fallback Messages When you send an RCS message with a fallback configured and the RCS message cannot be delivered (e.g., the recipient's device doesn't support RCS), the system will automatically send the fallback SMS/MMS message instead. ### Webhook routing Fallback events are delivered to two different webhooks: 1. **RCS agent's webhook** receives a `MESSAGE.STATUS` event with: * `status`: `FALLBACK_SENT` * `fallbackMessage`: Details of the SMS/MMS message that was sent instead, including its message ID, type, sender, recipient, text, and any media URLs. 2. **Fallback phone number's webhook** receives `MESSAGE.STATUS` events related to the fallback message that was sent. > One API for RCS, iMessage, MMS, and SMS. Build, test, and scale every channel — send your first message in minutes, not weeks.