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

# Webhooks

> Verify webhook payloads in one line or via Express middleware

<Warning>
  To receive event data via webhook at your server URL, [book an appointment](https://calendly.com/tanfrompeerpop/30-minute-meeting) to get set up.
</Warning>

The SDK verifies incoming webhook requests using the **raw** request body and the `X-Webhook-Signature` header. Use your API key (e.g. `process.env.PEERPOP_API_KEY`) as the webhook secret.

## Webhook payload

After verification, the payload has this shape. Ticket events (`ticket.created`, `ticket.scanned`, and `ticket.refunded`) share the same fields: `ticket.refunded` is sent whenever a ticket is refunded.

```js theme={null}
{ 
  action: "ticket.created" || "ticket.scanned" || "ticket.refunded",
  amount: 25.00, // purchase or refund amount for created/refunded, or 1 for scanned
  user: {
    username: "username",
    firstName "first",
    lastName: "last",
    phone: "5551234567",
    email: "attendee@example.com",
  },
  event: {
    eventId: "eventId",
    url: "https://peerpop.io/events/myevent",
    flyer: "https://peerpop.io/uploads/flyer.jpg",
    title: "Summer Party",
    date: "2025-07-15",
    time: "8:00 PM",
    address: "123 Main St, City, ST",
    venue: "The Venue",  // or false if not set
    organizer: "self",  // or Main Co-Host
    live: true
  },
  ticket: {
    uniqueId: "ticketId",
    title: "General Admission",
    price: 25.00,
    ticketTypeId: "ticketTypeId"
  },
  ticketId: "ticketId",
  metadata: { /* optional key-value data */ }
}
```

## One-line verify

Use the raw body (exact string or buffer as received) and the signature header:

```js theme={null}
const peerpop = require("peerpop");

// In your webhook handler (you must have access to the raw body)
const payload = peerpop.webhook.verify(
  req.rawBody,
  req.headers["x-webhook-signature"],
  process.env.PEERPOP_API_KEY
);
```

If the signature is missing, invalid, or expired, `verify()` throws. Catch and return 400:

```js theme={null}
try {
  const payload = peerpop.webhook.verify(
    req.rawBody,
    req.headers["x-webhook-signature"],
    process.env.PEERPOP_API_KEY
  );
  // use payload
} catch (err) {
  res.status(400).send(err.message);
}
```

Errors have `err.code` set to `PEERPOP_WEBHOOK_INVALID` or `PEERPOP_WEBHOOK_EXPIRED`.

## Express middleware

Verification must run on the **raw** body. Use `express.raw()` for the webhook route so the body is not parsed as JSON before verification:

```js theme={null}
const express = require("express");
const peerpop = require("peerpop");

const app = express();

app.post(
  "/webhook",
  express.raw({ type: "application/json" }),
  peerpop.webhook.middleware(process.env.PEERPOP_API_KEY),
  (req, res) => {
    const payload = req.webhookPayload; // already verified and parsed
    console.log(payload);
    res.send("OK");
  }
);
```

If verification fails, the middleware responds with 400 and does not call your route handler.

## API

**`peerpop.webhook.verify(rawBody, signatureHeader, secret)`**

Returns the parsed payload object, or throws if the signature is invalid or expired. Use when you have the raw body string (or buffer converted to string).

**`peerpop.webhook.middleware(secret)`**

Express middleware that verifies the request and sets `req.webhookPayload`. Use with `express.raw({ type: "application/json" })` on that route.

## Signature format

PeerPop sends `X-Webhook-Signature: t=,v1= `. The HMAC is over `timestamp + "." + rawBody` with your webhook secret. Signatures older than 5 minutes are rejected (replay protection).
