# Webhooks

> Get a signed HTTPS call when trips, quotes, travelers or documents change.

bymundi calls your HTTPS endpoint when something changes — whoever changed it: someone in the app, the assistant, or the API.

## Subscribe

Create an endpoint with your key (or in Integrations → API & MCP → Webhooks):

```bash
curl -X POST https://api.bymundi.com/v1/webhook-endpoints \
  -H "Authorization: Bearer $BYMUNDI_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"url":"https://example.com/hooks/bymundi","eventTypes":["trip.updated","quote.accepted"]}'
```

The answer includes the endpoint's **signing secret** (`whsec_…`), **shown once**. An endpoint belongs to the key that created it: it reads with that key's permissions and stops when the key is revoked. A key can have up to 10.

## The event

```json
{
  "id": "evt_…",
  "type": "trip.updated",
  "timestamp": "2027-05-01T09:00:00Z",
  "subject": { "object": "trip", "id": "…", "tripId": "…" },
  "changed": ["title", "startDate"],
  "data": { "object": "trip", "id": "…", "title": "…" }
}
```

`data` is the object exactly as its REST `GET` returns it, read when the event is delivered. `changed` names the fields that changed. Bursts of edits are grouped: an `*.updated` event is sent 5 seconds after the last edit, and at most 60 seconds after the first.

## Verifying signatures

Deliveries follow [Standard Webhooks](https://www.standardwebhooks.com). Three headers:

- `webhook-id` — the event id; the same on every retry.
- `webhook-timestamp` — Unix seconds.
- `webhook-signature` — `v1,<base64>`; during a secret rotation, two space-separated signatures.

The signature is HMAC-SHA256 over `{webhook-id}.{webhook-timestamp}.{raw body}`, keyed with the base64-decoded part of your secret after `whsec_`. Verify against the **raw** body, before parsing it, and refuse timestamps more than 5 minutes away:

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

export function verify(secret, headers, rawBody) {
  const id = headers["webhook-id"], ts = headers["webhook-timestamp"];
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = crypto.createHmac("sha256", key).update(`${id}.${ts}.${rawBody}`).digest("base64");
  return headers["webhook-signature"].split(" ").some((s) => {
    const sig = s.split(",")[1] ?? "";
    return sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
  });
}
```

Any Standard Webhooks library (JavaScript, Python, Go, Ruby, PHP…) does the same.

## Answering

Answer any `2xx` within 10 seconds; do slow work afterwards. Anything else — or no answer — is retried after 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 10 hours. An endpoint with no successful delivery for 3 days is switched off; turn it back on with `PATCH` and `enabled: true`.

- **Delivery is at least once.** Deduplicate by `webhook-id`.
- **Order is not guaranteed.** Compare `timestamp`, or re-read the object.
- **Missed events** stay available for 30 days at `GET /v1/events`.

## Events

- [`trip.created`](https://api.bymundi.com/docs/webhooks/events/trip.created.md) — A trip was created.
- [`trip.updated`](https://api.bymundi.com/docs/webhooks/events/trip.updated.md) — A trip's fields or metadata changed. `changed` names the fields.
- [`trip.archived`](https://api.bymundi.com/docs/webhooks/events/trip.archived.md) — A trip was archived. `data` is null: an archived trip is not readable (see GET /trips/archived).
- [`trip.restored`](https://api.bymundi.com/docs/webhooks/events/trip.restored.md) — An archived trip was restored.
- [`trip.published`](https://api.bymundi.com/docs/webhooks/events/trip.published.md) — A trip was published to its travelers.
- [`trip.unpublished`](https://api.bymundi.com/docs/webhooks/events/trip.unpublished.md) — A trip was unpublished.
- [`trip.deleted`](https://api.bymundi.com/docs/webhooks/events/trip.deleted.md) — A trip was erased for good. `data` is null.
- [`trip.itinerary.updated`](https://api.bymundi.com/docs/webhooks/events/trip.itinerary.updated.md) — A trip's itinerary changed — one event per burst of edits. `data` is the TRIP; read the tree with GET /trips/{tripId}/itinerary.
- [`quote.created`](https://api.bymundi.com/docs/webhooks/events/quote.created.md) — A quote was created.
- [`quote.updated`](https://api.bymundi.com/docs/webhooks/events/quote.updated.md) — A quote changed. `changed` names the fields.
- [`quote.sent`](https://api.bymundi.com/docs/webhooks/events/quote.sent.md) — A quote was sent (or sent again).
- [`quote.accepted`](https://api.bymundi.com/docs/webhooks/events/quote.accepted.md) — A quote was accepted.
- [`quote.rejected`](https://api.bymundi.com/docs/webhooks/events/quote.rejected.md) — A quote was rejected.
- [`quote.deleted`](https://api.bymundi.com/docs/webhooks/events/quote.deleted.md) — A quote was deleted. `data` is null.
- [`traveler.added`](https://api.bymundi.com/docs/webhooks/events/traveler.added.md) — A passenger was added to a trip. Personal data.
- [`traveler.updated`](https://api.bymundi.com/docs/webhooks/events/traveler.updated.md) — A passenger changed. `changed` names the fields. Personal data.
- [`traveler.removed`](https://api.bymundi.com/docs/webhooks/events/traveler.removed.md) — A passenger was removed. `data` is null.
- [`trip.contact.updated`](https://api.bymundi.com/docs/webhooks/events/trip.contact.updated.md) — A trip's booking contact was made, changed or removed (`data` null when there is none). Personal data.
- [`document.created`](https://api.bymundi.com/docs/webhooks/events/document.created.md) — A document finished uploading.
- [`document.updated`](https://api.bymundi.com/docs/webhooks/events/document.updated.md) — A document was renamed, moved or released. `changed` names the fields.
- [`document.deleted`](https://api.bymundi.com/docs/webhooks/events/document.deleted.md) — A document was deleted. `data` is null.

## Operations

- [List webhook endpoints](https://api.bymundi.com/docs/reference/webhooks.endpoints.list.md) — `GET /v1/webhook-endpoints`
- [Create a webhook endpoint](https://api.bymundi.com/docs/reference/webhooks.endpoints.create.md) — `POST /v1/webhook-endpoints`
- [Get a webhook endpoint](https://api.bymundi.com/docs/reference/webhooks.endpoints.get.md) — `GET /v1/webhook-endpoints/{endpointId}`
- [Update a webhook endpoint](https://api.bymundi.com/docs/reference/webhooks.endpoints.update.md) — `PATCH /v1/webhook-endpoints/{endpointId}`
- [Delete a webhook endpoint](https://api.bymundi.com/docs/reference/webhooks.endpoints.delete.md) — `DELETE /v1/webhook-endpoints/{endpointId}`
- [Rotate a webhook endpoint's secret](https://api.bymundi.com/docs/reference/webhooks.endpoints.rotateSecret.md) — `POST /v1/webhook-endpoints/{endpointId}/rotate-secret`
- [Send a test event](https://api.bymundi.com/docs/reference/webhooks.endpoints.test.md) — `POST /v1/webhook-endpoints/{endpointId}/test`
- [List an endpoint's deliveries](https://api.bymundi.com/docs/reference/webhooks.deliveries.list.md) — `GET /v1/webhook-endpoints/{endpointId}/deliveries`
- [Resend a delivery](https://api.bymundi.com/docs/reference/webhooks.deliveries.resend.md) — `POST /v1/webhook-endpoints/{endpointId}/deliveries/{deliveryId}/resend`
- [List events](https://api.bymundi.com/docs/reference/events.list.md) — `GET /v1/events`
- [Get an event](https://api.bymundi.com/docs/reference/events.get.md) — `GET /v1/events/{eventId}`
