<!-- Reviews Webhooks: Events, Payloads and Signatures · Prisma · https://prismareviews.com/docs/api-reference/webhooks -->
> Prisma webhooks: register endpoints with /v1/webhooks, get review.created, review.updated, reply.published, reply.failed and connection.updated, and verify the HMAC signature.

# Webhooks

Webhooks tell your server when something changes, so you don't have to poll. You register an HTTPS endpoint with the API, choose the events, and Prisma sends a signed `POST` for each one.

## Events

| Event | When | `data` |
| --- | --- | --- |
| `review.created` | A new review is found on a connected location. | `id` (review), `location_id`, `platform` |
| `review.updated` | A review's content changed on the platform, or a reply appeared or changed there. | `id`, `location_id`, `platform` |
| `reply.published` | A reply sent through Prisma is live on the platform. | `id` (review), `location_id`, `platform` |
| `reply.failed` | The platform refused a reply, or every retry failed. | `id` (review), `platform`, `error` |
| `connection.updated` | An OAuth connection became `active`, or a connection became `reauth_required`. | `id` (connection), `platform`, `status`, plus `locations_created` or `reason` |

Events carry IDs, not full objects. Fetch the current state with [`GET /v1/reviews/{id}`](/docs/api-reference/reviews) or [`GET /v1/connections`](/docs/api-reference/connections).

## Register an endpoint

`POST /v1/webhooks`

| Body param | Required | Description |
| --- | --- | --- |
| `url` | yes | Your endpoint. Must start with `https://`. |
| `events` | no | Array of event types, or `["*"]` for all of them (the default). At least one if you send it. |

```bash
curl -X POST https://api.prismareviews.com/v1/webhooks \
  -H "Authorization: Bearer $PRISMA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://yourapp.com/webhooks/prisma", "events": ["review.created", "reply.failed", "connection.updated"]}'
```
```javascript
const res = await fetch("https://api.prismareviews.com/v1/webhooks", {
  method: "POST",
  headers: {
    Authorization: "Bearer " + process.env.PRISMA_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    url: "https://yourapp.com/webhooks/prisma",
    events: ["review.created", "reply.failed", "connection.updated"],
  }),
});
const endpoint = await res.json();
// Store endpoint.secret now: it is never shown again
```
```python
import os
import requests

resp = requests.post(
    "https://api.prismareviews.com/v1/webhooks",
    json={
        "url": "https://yourapp.com/webhooks/prisma",
        "events": ["review.created", "reply.failed", "connection.updated"],
    },
    headers={"Authorization": f"Bearer {os.environ['PRISMA_API_KEY']}"},
    timeout=30,
)
endpoint = resp.json()
print(endpoint["secret"])  # store it now: it is never shown again
```

**Response `201`**

```json
{
  "id": "we_01KJ8R1A3B5C7D9E1F3G5H7J9K",
  "url": "https://yourapp.com/webhooks/prisma",
  "events": ["review.created", "reply.failed", "connection.updated"],
  "secret": "whsec_3q2Xk9LmP0aB7cD1eF4gH6jK8mN2pQ5r",
  "created_at": "2026-10-21T09:20:00.000Z"
}
```

The signing `secret` is returned only in this response. Prisma stores it encrypted and never shows it again.

Webhook endpoints belong to the workspace, not to a key: endpoints created with a test key and with a live key are the same list, and each endpoint receives events from both modes. Fetch the object with a key of the matching mode (a test review returns `404` to a live key).

## List endpoints

`GET /v1/webhooks`

```bash
curl https://api.prismareviews.com/v1/webhooks \
  -H "Authorization: Bearer $PRISMA_API_KEY"
```

```json
{
  "data": [
    {
      "id": "we_01KJ8R1A3B5C7D9E1F3G5H7J9K",
      "url": "https://yourapp.com/webhooks/prisma",
      "events": ["review.created", "reply.failed", "connection.updated"],
      "active": true,
      "created_at": "2026-10-21T09:20:00.000Z"
    }
  ]
}
```

## Delete an endpoint

`DELETE /v1/webhooks/{id}`

```bash
curl -X DELETE https://api.prismareviews.com/v1/webhooks/we_01KJ8R1A3B5C7D9E1F3G5H7J9K \
  -H "Authorization: Bearer $PRISMA_API_KEY"
```

```json
{ "id": "we_01KJ8R1A3B5C7D9E1F3G5H7J9K", "deleted": true }
```

`404 resource_not_found` if there is no endpoint with that ID in your workspace. Deliveries still pending for it are dropped. There is no update endpoint: to change the URL or the events, create a new endpoint and delete the old one.

## The delivery

Each event is a `POST` with a JSON body:

```json
{
  "id": "evt_01KJ8S2B4C6D8E0F2G4H6J8K0M",
  "type": "review.created",
  "created_at": "2026-10-21T09:00:03.000Z",
  "data": {
    "id": "rev_01KJ8Q2M4T7HX3VB9CWD6NRZ5E",
    "location_id": "loc_01KJ8P9W2C4D6F8G0H2J4K6M8N",
    "platform": "google"
  }
}
```

More `data` examples:

```json
{ "id": "rev_01KJ8Q2M4T7HX3VB9CWD6NRZ5E", "platform": "google_play", "error": "Google Play API 400: ..." }
```

```json
{ "id": "con_01KJ8P7R5S3T1V9W7X5Y3Z1A2B", "platform": "google", "status": "reauth_required", "reason": "Google rejected the access token" }
```

Headers:

| Header | Value |
| --- | --- |
| `Content-Type` | `application/json` |
| `User-Agent` | `Prisma-Webhooks/1` |
| `Prisma-Signature` | `t=<unix seconds>,v1=<hex HMAC-SHA256>` |

Answer with any `2xx` status within 10 seconds. Do the slow work after answering, for example in a queue.

### Retries

A delivery fails on a non-`2xx` status, a timeout or a connection error. Failed deliveries are retried with exponential backoff: 30 seconds, then doubling up to one hour between attempts. Prisma stops after 30 attempts or when the event is older than your plan's retry window, whichever comes first:

| Plan | Retry window |
| --- | --- |
| Spark | 24 hours |
| Beam | 72 hours |
| Spectrum, Aurora | 7 days |

Each retry is signed again with a fresh timestamp and keeps the same event `id`. De-duplicate by `id`.

## Verify the signature

`Prisma-Signature` is `t=<timestamp>,v1=<signature>`, where the signature is the hex HMAC-SHA256 of `<timestamp>.<raw body>` with your endpoint's secret. Compute it over the raw request body, before any JSON parsing, compare in constant time and reject events whose timestamp is more than 5 minutes from your clock.

```javascript
import crypto from "node:crypto";

export function verifyPrismaSignature(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  if (!t || !parts.v1 || Math.abs(Date.now() / 1000 - t) > 300) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
```
```python
import hashlib
import hmac
import time

def verify_prisma_signature(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t, sig = parts.get("t"), parts.get("v1")
    if not t or not sig or abs(time.time() - int(t)) > 300:
        return False
    expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig)
```

With Express, read the raw body with `express.raw({ type: "application/json" })` on the webhook route, verify, then `JSON.parse` it.

## FAQ

### Why doesn't the webhook include the whole review?

Events carry IDs only, so a retried or late event never overwrites newer data. Fetch the current object with `GET /v1/reviews/{id}`.

### Can I get the signing secret again?

No. It is shown once, when you create the endpoint. If you lose it, delete the endpoint and create a new one.

### Can events arrive twice or out of order?

Yes, as with most webhook systems. A delivery that timed out is retried with the same event `id`, so de-duplicate by `id` and use `created_at` to order events.
