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

# Webhooks

> Receive real-time notifications when events occur in your organization

Webhooks let you subscribe to events in Beach Depository so your systems can react in real time -- no polling required. When an event occurs (e.g., inventory received, shipment delivered), Beach Depository sends an HTTP POST to your registered URL with a signed JSON payload.

## How it works

```
Event occurs (e.g. shipment delivered)
└── Beach Depository dispatches webhook
    ├── Signs payload with HMAC-SHA256
    ├── POSTs to your registered URL
    ├── Retries on failure (up to 3 attempts)
    └── Logs delivery status
```

1. Register a webhook endpoint in **Settings > Webhooks** or via the API
2. Select which event types to subscribe to
3. Store the signing secret securely -- it is only shown once
4. Beach Depository POSTs to your URL whenever a subscribed event fires

## Creating a webhook

### Via the dashboard

Navigate to **Settings > Webhooks** and click **Create Webhook**. Enter a name, your endpoint URL, and select the events you want to receive. After creation, copy and store the signing secret.

### Via the API

```bash theme={null}
curl -X POST https://api.beachdepository.com/v1/webhooks \
  -H "Authorization: Bearer pv_live_your_api_key..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Inventory Notifications",
    "url": "https://example.com/webhooks/purevault",
    "events": ["inventory.received", "outbound_shipment.delivered"]
  }'
```

Response:

```json theme={null}
{
  "data": {
    "id": "wh_abc123",
    "signingSecret": "whsec_d4d6eb900e291d584a235aeebff1aabe..."
  }
}
```

<Note>
  The signing secret is only returned once at creation time. Store it securely -- you will not be
  able to retrieve it again.
</Note>

## Event types

| Event                              | Description                                   |
| ---------------------------------- | --------------------------------------------- |
| `inventory.received`               | An inventory item was received into the vault |
| `inventory.status_changed`         | An inventory item's status changed            |
| `inventory.valuation_changed`      | A new valuation was recorded for a product    |
| `inbound_shipment.status_changed`  | An inbound shipment's status changed          |
| `outbound_shipment.status_changed` | An outbound shipment's status changed         |
| `outbound_shipment.delivered`      | An outbound shipment was delivered            |
| `outbound_request.status_changed`  | An outbound request's status changed          |
| `transfer.status_changed`          | A transfer's status changed                   |
| `transfer.completed`               | A transfer was completed                      |
| `fbo_account.created`              | A new FBO account was created                 |
| `fbo_account.updated`              | An FBO account was updated                    |
| `statement.generated`              | A holding statement was generated             |

## Payload format

All webhook payloads follow the same structure:

```json theme={null}
{
  "event": "outbound_shipment.delivered",
  "timestamp": 1709654321,
  "data": {
    "id": "ship_abc123",
    "organizationId": "org_xyz",
    "fboAccountId": "fbo_456",
    "deliveredAt": 1709654321000
  }
}
```

The `data` object varies by event type but always includes `id` and `organizationId`.

## Signature verification

Every webhook request includes an `X-PureVault-Signature` header for verifying authenticity:

```
X-PureVault-Signature: t=1709654321,v1=5257a869e7ecebeda32affa62cdca3fa51...
```

The signature is an HMAC-SHA256 hash of `{timestamp}.{payload}` using your signing secret.

### Verifying in Node.js

```javascript theme={null}
import crypto from "crypto"

const MAX_TIMESTAMP_AGE_SECONDS = 300 // 5 minutes

function verifyWebhookSignature(payload, signatureHeader, secret) {
  const parts = Object.fromEntries(signatureHeader.split(",").map((p) => p.split("=", 2)))

  // Reject stale timestamps to prevent replay attacks
  const timestamp = parseInt(parts.t, 10)
  const age = Math.floor(Date.now() / 1000) - timestamp
  if (isNaN(timestamp) || age > MAX_TIMESTAMP_AGE_SECONDS || age < 0) {
    return false
  }

  const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${payload}`).digest("hex")

  // Constant-time comparison to prevent timing attacks
  const a = Buffer.from(expected, "hex")
  const b = Buffer.from(parts.v1 || "", "hex")
  if (a.length !== b.length) return false
  return crypto.timingSafeEqual(a, b)
}

// In your webhook handler
app.post("/webhooks/purevault", (req, res) => {
  const payload = JSON.stringify(req.body)
  const signature = req.headers["x-purevault-signature"]

  if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send("Invalid signature")
  }

  // Process the event
  const { event, data } = req.body
  console.log(`Received ${event}:`, data)

  res.status(200).send("OK")
})
```

### Verifying in Python

```python theme={null}
import hmac
import hashlib
import time

MAX_TIMESTAMP_AGE_SECONDS = 300  # 5 minutes

def verify_webhook_signature(payload: bytes, signature_header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in signature_header.split(","))

    # Reject stale timestamps to prevent replay attacks
    timestamp = int(parts.get("t", "0"))
    age = int(time.time()) - timestamp
    if age > MAX_TIMESTAMP_AGE_SECONDS or age < 0:
        return False

    expected = hmac.new(
        secret.encode(),
        f"{parts['t']}.{payload.decode()}".encode(),
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(parts["v1"], expected)
```

## Request headers

Each webhook delivery includes these headers:

| Header                   | Description                                |
| ------------------------ | ------------------------------------------ |
| `Content-Type`           | `application/json`                         |
| `X-PureVault-Signature`  | HMAC-SHA256 signature for verification     |
| `X-PureVault-Event-Id`   | Unique event ID for idempotency            |
| `X-PureVault-Event-Type` | The event type (e.g. `inventory.received`) |
| `User-Agent`             | `PureVault-Webhooks/1.0`                   |

Use `X-PureVault-Event-Id` to deduplicate events if your endpoint receives the same delivery more than once during retries.

## Retry policy

If your endpoint returns a non-2xx status code or times out (10 seconds), Beach Depository retries with exponential backoff:

| Attempt | Delay      |
| ------- | ---------- |
| 1       | Immediate  |
| 2       | 60 seconds |
| 3       | 5 minutes  |

After 3 failed attempts, the delivery is marked as failed. You can view delivery history in the dashboard under **Settings > Webhooks > View Deliveries**.

## Best practices

<AccordionGroup>
  <Accordion title="Respond quickly">
    Return a `200` response as fast as possible. Process the event asynchronously (e.g., add it to a
    queue) rather than performing heavy work in the request handler. Beach Depository times out
    after 10 seconds.
  </Accordion>

  <Accordion title="Handle duplicates">
    Use the `X-PureVault-Event-Id` header to deduplicate. In rare cases, the same event may be
    delivered more than once during retries.
  </Accordion>

  <Accordion title="Verify signatures">
    Always verify the `X-PureVault-Signature` header before processing a webhook. This ensures the
    request came from Beach Depository and was not tampered with.
  </Accordion>

  <Accordion title="HTTPS required">
    Webhook URLs must use `https://`. The API rejects `http://` URLs and bare IP addresses.
    Endpoints must resolve to public hostnames -- private, loopback, and link-local addresses are
    blocked.
  </Accordion>

  <Accordion title="Monitor deliveries">
    Check the delivery log in the dashboard periodically. If your endpoint has been failing, fix the
    issue and consider re-processing missed events via the API.
  </Accordion>
</AccordionGroup>

## Testing locally

Use a tool like [webhook.site](https://webhook.site) or [ngrok](https://ngrok.com) to receive webhooks during development:

```bash theme={null}
# Start ngrok tunnel
ngrok http 3000

# Use the ngrok URL as your webhook endpoint
# https://abc123.ngrok-free.app/webhooks/purevault
```
