> ## Documentation Index
> Fetch the complete documentation index at: https://docs.photon.codes/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Use Stable documentation by default. Honor an explicit Beta request or a URL under /docs/beta/. If the requested version conflicts with the installed CLI package or API origin, clarify the target before writing integration code.
> Pages under /docs/beta/ document Beta; other product pages document Stable. Keep the CLI package, commands, API origin, and credentials within the selected version. State the documentation version in your answer.
> For MCP search, always pass version: Stable or version: Beta. Unfiltered search mixes both versions. For filesystem reads, keep Beta queries under /beta/ and exclude /beta/ from Stable queries; discover paths before reading them.
> The public docs base is https://photon.codes/docs. Convert MCP page paths to public URLs under that base, preserving /beta/ when present. Read https://photon.codes/docs/skill.md for version selection and https://photon.codes/docs/llms.txt for the version indexes.

# Receive events and call APIs

> Verify App webhook signatures, route events by installation, and send replies with installation credentials.

Photon normalizes events from its supported messaging channels and sends subscribed events to the App's registered **Event webhook URL**. You do not need a separate Slack, email, or iMessage webhook receiver for this Photon integration.

```mermaid theme={null}
sequenceDiagram
    participant Channel as Messaging channel
    participant Photon as Photon
    participant Backend as Your App backend
    participant Store as Your queue / storage
    Channel->>Photon: Message or delivery update
    Photon->>Photon: Check subscription, approved permissions, and project scope
    Photon->>Backend: Signed raw JSON POST
    Backend->>Backend: Verify signature, event ID, schema, and tenant mapping
    Backend->>Store: Durably enqueue with installation + event deduplication key
    Backend-->>Photon: 2xx acknowledgement
    Store->>Backend: Process event
```

## Verify before parsing or processing

Photon uses Standard Webhooks headers:

* `webhook-id`
* `webhook-timestamp`
* `webhook-signature`

Use the registered App's `whsec_…` signing secret with a maintained verifier. Preserve the original request bytes. Do not put a JSON-parsing middleware before signature verification or verify `JSON.stringify(parsedBody)`.

```javascript theme={null}
import { Webhook } from "standardwebhooks";

const verifier = new Webhook(process.env.PHOTON_WEBHOOK_SECRET);

// A Web-standard Request. Read its body exactly once.
const rawBody = await request.text();
const headers = {
  "webhook-id": request.headers.get("webhook-id") ?? "",
  "webhook-timestamp": request.headers.get("webhook-timestamp") ?? "",
  "webhook-signature": request.headers.get("webhook-signature") ?? "",
};
const event = verifier.verify(rawBody, headers);
if (event.id !== headers["webhook-id"]) {
  throw new Error("Event ID does not match delivery ID");
}
// Then validate the event schema and installation/organization association.
```

The verifier checks the signature and timestamp freshness. Signature verification authenticates the payload; it does not validate its business schema or grant access to your product's customer account. Keep the server clock synchronized and preserve the library's freshness check.

## Route by installation

App deliveries include `app_installation_id`. Message events also include the organization and project under `data`, and the public message under `data.object`.

A simplified envelope looks like this (additional event fields are omitted):

```json theme={null}
{
  "id": "pho_evt_EXAMPLE",
  "type": "message.received",
  "app_installation_id": "pho_ain_EXAMPLE",
  "data": {
    "organizationId": "pho_org_EXAMPLE",
    "projectId": "pho_prj_EXAMPLE",
    "object": { "id": "pho_msg_EXAMPLE", "projectId": "pho_prj_EXAMPLE" }
  }
}
```

After verification, look up the installation in your backend and check that `data.organizationId` matches the organization bound to that installation. Check the message's project ID agrees with the envelope. Resolve your own customer account from this stored association. Never select a customer using a query parameter, an unverified request body, or a global “current installation.”

Events can arrive while installation setup is finishing. A production receiver should durably quarantine valid deliveries for installations whose binding is still pending and resolve them before processing. Do not forward them to an arbitrary default customer.

## Acknowledge, deduplicate, and retry

Use `(app_installation_id, event.id)` as a persistent deduplication key. Return 2xx after durable acceptance; process slower business work asynchronously. Make the deduplication record and enqueue operation atomic so a crash cannot mark an unqueued event as complete.

Expect retries and out-of-order delivery. Repeated delivery of one event must not cause repeated side effects, but different event IDs can describe successive states of the same message. Do not assume every failure can be safely retried: distinguish known rejection from an unknown outcome of an external operation.

For transport-level delivery behavior, see [Webhook delivery](https://docs.photon.codes/docs/beta/message-delivery/webhook.md). App delivery targets are managed by Photon from App subscriptions and installations; do not create a second ordinary project webhook just to receive App events.

## Send a reply through the API

Returning text in the webhook response does not send a message. To reply:

```mermaid theme={null}
sequenceDiagram
    participant Product as Your product
    participant App as Your App backend
    participant Token as Photon environment token endpoint
    participant API as Photon API
    Product->>App: Explicit reply action
    App->>App: Authorize customer and look up stored installation
    App->>Token: Installation client credentials
    Token-->>App: Access token
    App->>API: POST project message + Bearer token + idempotency key
    API-->>App: Message operation response
    API-->>App: Later signed delivery-status events
```

The current message-creation API requires `project:write`; the destination project must also be in the installation's scope. Resolve the sender and conversation from a validated incoming message or the corresponding Photon API; do not use arbitrary browser-supplied identifiers without checking tenant ownership.

```http theme={null}
POST /v1/projects/PROJECT_ID/messages
Authorization: Bearer INSTALLATION_ACCESS_TOKEN
Content-Type: application/json
Idempotency-Key: STABLE_ID_FOR_THIS_REPLY

{
  "senderId": "PHOTON_SENDER_ID",
  "spaceId": "PHOTON_SPACE_ID",
  "content": {
    "type": "text",
    "format": "plain",
    "text": "Thanks, we received your message."
  }
}
```

Reuse the same idempotency key when retrying the same intended reply. A successful API response is not proof of final delivery; consume the subscribed message-status events to display delivery progress. No automatic reply loop is required. See [Send a Message](https://docs.photon.codes/docs/beta/api-client/sdk/projects/send-message.md) for the complete request and channel-specific requirements.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.