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

# Webhook del Calendario

> Ricevi in tempo reale prenotazioni, spostamenti e annullamenti

# Webhook del Calendario

Dal pannello (Calendario → Impostazioni → Webhook) si registra un indirizzo **https** e si scelgono gli eventi. A ogni evento MyCentralino manda un **POST JSON** all'indirizzo; il tuo server deve rispondere **2xx entro 10 secondi**. Se non risponde o risponde con un errore, l'avviso viene ritentato dopo 1 minuto, 5 minuti, 30 minuti, 2 ore e 6 ore, poi abbandonato: lo stato di ogni consegna si legge nel pannello, con un bottone «Riprova».

## Eventi

| `event`             | `action`                                                          | Quando                                                                         |
| ------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `booking.created`   | `created`                                                         | un appuntamento è stato preso (pannello, agente AI, pagina pubblica, API)      |
| `booking.updated`   | `confirmed` · `rescheduled` · `updated` · `no_show` · `completed` | confermato, spostato, dati di chi prenota cambiati, segnato assente o concluso |
| `booking.cancelled` | `cancelled`                                                       | annullato (con `cancel_reason` se c'è)                                         |
| `booking.test`      | `test`                                                            | l'invio di prova dal pannello: un appuntamento di esempio con `id: 0`          |

## Payload

```json theme={null}
{
  "version": 1,
  "event": "booking.updated",
  "action": "rescheduled",
  "occurred_at": "2026-09-07T12:30:00+02:00",
  "booking": {
    "id": 91,
    "status": "confirmed",
    "calendar": {"id": 34, "slug": "luigi", "name": "Luigi"},
    "event": {"id": 12, "slug": "visita-nord", "name": "Visita", "duration_minutes": 60},
    "date": "2026-09-17", "time": "11:00", "end_date": "2026-09-17", "end_time": "12:00", "timezone": "Europe/Rome",
    "starts_at_utc": "2026-09-17T09:00:00Z", "ends_at_utc": "2026-09-17T10:00:00Z",
    "attendee": {"name": "Mario Rossi", "phone": "+393331234567", "email": null, "notes": null},
    "staff_notes": null, "answers": null,
    "source": "panel", "source_ref": null, "created_by": "titolare",
    "created_at": "2026-09-07T10:00:00+02:00", "updated_at": "2026-09-07T12:30:00+02:00",
    "cancelled_at": null, "cancel_reason": null, "cancelled_by": null
  },
  "previous": {"date": "2026-09-17", "time": "10:00"},
  "changes": null
}
```

`booking` ha **lo stesso formato** di [GET /v1/calendar/bookings/{id}](/mycentralino-api/calendar-bookings-detail). `previous` c'è solo allo spostamento, `changes` (elenco dei campi) solo alla modifica dei dati.

## Header e firma

| Header                     | Contenuto                                                                                        |
| -------------------------- | ------------------------------------------------------------------------------------------------ |
| `X-MyCentralino-Event`     | l'`event`                                                                                        |
| `X-MyCentralino-Delivery`  | ID della consegna, uguale a ogni nuovo tentativo: serve a scartare i doppioni                    |
| `X-MyCentralino-Timestamp` | epoch Unix (secondi) dell'invio                                                                  |
| `X-MyCentralino-Signature` | `sha256=` + HMAC-SHA256 esadecimale di `"{timestamp}.{corpo grezzo}"` con il segreto del webhook |
| `User-Agent`               | `MyCentralino-Calendario/1`                                                                      |

Il segreto (`whsec_…`) si vede una volta sola, alla creazione o quando lo si rigenera.

## Verifica della firma

<CodeGroup>
  ```php PHP theme={null}
  <?php
  $secret = 'whsec_...';
  $body = file_get_contents('php://input');
  $ts = $_SERVER['HTTP_X_MYCENTRALINO_TIMESTAMP'] ?? '';
  $sig = $_SERVER['HTTP_X_MYCENTRALINO_SIGNATURE'] ?? '';
  $expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $body, $secret);
  if (! ctype_digit($ts) || abs(time() - (int) $ts) > 300 || ! hash_equals($expected, $sig)) {
      http_response_code(401);
      exit;
  }
  $payload = json_decode($body, true);
  http_response_code(200);   // rispondi subito, elabora dopo
  ```

  ```python Python theme={null}
  import hmac, hashlib, time
  from flask import Flask, request, abort

  app = Flask(__name__)
  SECRET = b'whsec_...'

  @app.post('/webhook/mycentralino')
  def hook():
      ts = request.headers.get('X-MyCentralino-Timestamp', '')
      sig = request.headers.get('X-MyCentralino-Signature', '')
      expected = 'sha256=' + hmac.new(SECRET, f'{ts}.'.encode() + request.get_data(), hashlib.sha256).hexdigest()
      if not ts.isdigit() or abs(time.time() - int(ts)) > 300 or not hmac.compare_digest(expected, sig):
          abort(401)
      payload = request.get_json()
      return '', 200   # rispondi subito, elabora dopo
  ```
</CodeGroup>

## Regole pratiche

* Rispondi 2xx **prima** di fare lavoro lungo.
* Usa `X-MyCentralino-Delivery` (o `booking.id` + `action` + `occurred_at`) per non elaborare due volte lo stesso avviso.
* Rifiuta timestamp più vecchi di 5 minuti.
* L'indirizzo deve essere raggiungibile da Internet in **https**: niente reti private, niente credenziali nell'URL, i redirect non vengono seguiti.
