# Signed booking webhooks

Canonical human page: https://aune-homepage-chat.vercel.app/docs/webhooks
Canonical machine page: https://aune-homepage-chat.vercel.app/ai/webhooks.md

Webhook delivery is driven by the transactional booking outbox. A booking remains confirmed even if notification delivery fails; delivery state is never booking truth.

## Facts

- **Manage scope:** aune.webhooks.manage
- **Signature:** HMAC-SHA256 over id.timestamp.raw-body
- **Timeout:** 8 seconds
- **Redirects:** Rejected
- **Secret:** Shown once

## Register an endpoint

POST `/v1/webhook-endpoints` with an OAuth token carrying `aune.webhooks.manage`. Destinations must use public HTTPS on port 443; Aune resolves DNS and rejects private, loopback, link-local, multicast, local, and internal targets.

### Endpoint registration

```bash
curl -X POST "https://aune-homepage-chat.vercel.app/v1/webhook-endpoints" \
  -H "Authorization: Bearer $AUNE_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{"url":"https://agent.example/aune/webhooks","event_types":["booking.confirmed","booking.rescheduled","booking.cancelled"]}'
```

## Verify before parsing

Read the exact raw bytes. Reject stale timestamps, compare signatures in constant time, and deduplicate `Webhook-Id`. Only then parse JSON or trigger side effects.

### Node.js verification core

```typescript
import { createHmac, timingSafeEqual } from "node:crypto";

const signed = `${webhookId}.${webhookTimestamp}.${rawBody}`;
const expected = `v1,${createHmac("sha256", secret)
  .update(signed)
  .digest("base64url")}`;

const valid = Math.abs(Date.now() / 1000 - Number(webhookTimestamp)) <= 300 &&
  timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
```

## Event contract

- `booking.confirmed`: durable booking committed.
- `booking.rescheduled`: same booking moved to a newly revalidated option.
- `booking.cancelled`: booking lifecycle status changed to cancelled.
- `booking.provider_action_required`: provider-side follow-up is required.
- Replay through `POST /v1/webhook-events/{outbox_id}/replay`; ownership is bound to the OAuth client.
