Portale sviluppatori

Una chiave, e la sala parla con il tuo software.

buked espone una superficie pubblica versionata — /api/public/v1 — pensata per i gestionali, i portali di prenotazione, i software di sala e gli agenti AI. Stessa identità per tutti: una chiave di integrazione emessa dall’esercente, con permessi espliciti e revocabile in qualunque momento.

Una chiave, tre superfici

Tutto parte da un concetto solo: la credenziale di integrazione. È emessa dall’esercente dalla propria console, porta gli scope che lui ha deciso di concedere, ed è sempre legata a una singola attività — non esistono chiavi «di piattaforma». Il server ne conserva solo l’impronta: il valore in chiaro esiste una volta sola, nella risposta alla creazione.

Sopra questa identità stanno tre superfici, che condividono scope, errori e audit:

SuperficiePer chiTrasporto
/api/public/v1gestionali, portali, software di salaREST + webhook firmati
/mcpagenti AI, concierge, assistenti dell’esercenteMCP su HTTP (JSON-RPC 2.0)
/api/public/v1/posregistratori di cassa e gestionali di cassaREST + webhook

Non c’è una porta di servizio: MCP e cassa passano dagli stessi scope e dagli stessi controlli di qualunque altra integrazione.

Ottenere una chiave

Le chiavi non si chiedono a noi: le emette l’esercente, dalla sezione «Integrazioni» della sua console. È lui il titolare dei dati dei propri ospiti, ed è lui che autorizza chi vi accede. Se stai integrando per conto di un cliente, chiedigli di creare la chiave e di consegnartela — con i soli permessi che ti servono.

  1. Il proprietario dell’attività entra in buked.it/console.
  2. Apre Integrazioni (la sezione è riservata al ruolo proprietario).
  3. Dà un nome alla chiave — quello che gli ricorderà chi la usa — e spunta i permessi.
  4. Copia il valore in chiaro: compare una volta sola. Se si perde, l’unica strada è revocarlo ed emetterne un altro.

Forma della chiave: bk_live_ seguito da 32 caratteri base64url (192 bit di casualità). Il prefisso parlante serve a farla riconoscere a colpo d’occhio — e ai rilevatori di segreti a fermarla prima che finisca in un repository pubblico.

Una chiave è una credenziale a tutti gli effetti: sta sul tuo server, in una variabile d’ambiente o in un gestore di segreti. Mai nel codice di un’app, mai in una pagina web, mai in un repository.

Autenticazione

Ogni richiesta porta la chiave nell’intestazione Authorization, schema Bearer. Non ci sono altri parametri da passare: il tenant e i permessi li stabilisce il gateway a partire dalla chiave, e li propaga ai servizi. Nessun header di contesto inviato dal client viene mai preso in considerazione.

richiesta
curl -s https://buked.it/api/public/v1/availability \
  --get \
  --data-urlencode "date=2026-09-20" \
  --data-urlencode "service=EVENING" \
  --data-urlencode "size=4" \
  -H "Authorization: Bearer bk_live_7Qd2mXr9KpZa4B6cD8eF0gHj"

È accettato anche l’header X-Api-Key, utile ai client che non sanno mandare Authorization. Authorization resta la forma preferita.

Errori

Tutti gli errori hanno la stessa forma di tutta la piattaforma: un code stabile su cui programmare e un message in italiano che dice cosa fare, pensato per essere mostrato a una persona.

401 — chiave assente, sconosciuta o revocata
{
  "code": "API_KEY_INVALID",
  "message": "Chiave non valida o revocata"
}
403 — la chiave esiste ma non ha quel permesso
{
  "code": "SCOPE_REQUIRED",
  "message": "Alla chiave manca il permesso bookings:write"
}

Un 502 AUTH_UNAVAILABLE significa che non siamo riusciti a validare la chiave: è un guasto momentaneo, si ritenta. Un 404 UNKNOWN_ROUTE significa che il percorso non esiste su questa superficie.

Idempotenza

Ogni scrittura accetta Idempotency-Key, e va usata sempre: un rilancio dopo un timeout non deve produrre una seconda pre-prenotazione o chiudere due volte lo stesso tavolo. Usa un valore stabile e legato al fatto (il numero dello scontrino, un UUID generato prima del primo tentativo), non uno nuovo a ogni retry.

Permessi

Gli scope sono il contratto fra l’esercente e te: quello che non ti concede non è raggiungibile nemmeno per errore. Il controllo avviene due volte — al gateway, che blocca la rotta prima ancora del servizio, e nel servizio stesso.

ScopeCosa sblocca
availability:readLeggere disponibilità e orari: fasce aperte, posti e tavoli liberi, prossimo round. Include calendario e tavoli.
bookings:readLeggere le prenotazioni: comitive del giorno, stato dei tavoli, nomi di chi ha prenotato.
bookings:writePrenotare e modificare: pre-prenotazioni, walk-in, lista d’attesa, cambi di stato.
events:readLeggere gli eventi pubblicati dall’attività e ciò che sta sotto /events.
pos:writeIl profilo cassa: mappatura dei tavoli, conto aperto, conto pagato.
webhooks:manageCreare, elencare e togliere le sottoscrizioni webhook, e leggerne la traccia di consegna.

Nota per chi integra il lato esercente: le letture sotto /events chiedono events:read anche quando riguardano le prenotazioni di un evento. Una chiave da «assistente del ristoratore» tiene di solito availability:read, bookings:read ed events:read.

API pubblica

Base: https://buked.it/api/public/v1. Le date sono YYYY-MM-DD, gli orari HH:MM nel fuso dell’attività, gli istanti ISO-8601 in UTC. Le fasce di servizio sono MORNING, LUNCH, AFTERNOON, EVENING. Le zone sono codici propri di ogni attività (INDOOR, OUTDOOR, TERRACE, PRIVATE sono i quattro storici, ma un locale può averne di suoi).

Disponibilità

GET /availabilityavailability:read. È una fotografia di questo istante, non una promessa: i posti sono contesi e cambiano di secondo in secondo. Non impegna nulla.

richiesta
curl -s https://buked.it/api/public/v1/availability \
  --get \
  --data-urlencode "date=2026-09-20" \
  --data-urlencode "service=EVENING" \
  --data-urlencode "size=4" \
  -H "Authorization: Bearer bk_live_7Qd2mXr9KpZa4B6cD8eF0gHj"
risposta 200
{
  "date": "2026-09-20",
  "size": 4,
  "services": [
    {
      "service": "EVENING",
      "label": "Cena",
      "startTime": "19:00",
      "endTime": "23:00",
      "expectedDurationMinutes": 105,
      "slots": [
        { "time": "20:15", "freeSeats": 22, "freeTables": 5,
          "feasible": true,  "hot": false },
        { "time": "20:30", "freeSeats": 10, "freeTables": 2,
          "feasible": true,  "hot": true },
        { "time": "20:45", "freeSeats": 3,  "freeTables": 1,
          "feasible": false, "hot": false }
      ],
      "nextRound": "21:40"
    }
  ]
}

freeSeats sono i posti residui, freeTables i tavoli utili; feasible dice se la comitiva richiesta ci sta davvero; hot segnala gli orari che in questo momento molti stanno guardando. nextRound è la stima dell'ora in cui si libera il prossimo giro di tavoli — una previsione, da riferire come tale.

Pre-prenotazione (hold)

POST /holdsbookings:write. Una pre-prenotazione non è una prenotazione: il motore mette da parte la capacità con una scadenza breve e restituisce un otpUrl. La prenotazione diventa reale solo quando una persona apre quel link dal proprio telefono e conferma. Se l’orario chiesto era pieno il motore propone il primo utile e lo dichiara con shifted.

richiesta
curl -s -X POST https://buked.it/api/public/v1/holds \
  -H "Authorization: Bearer bk_live_7Qd2mXr9KpZa4B6cD8eF0gHj" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 3f1c9a52-7b0e-4d21-9c8a-11d0f6b4e7a2" \
  -d '{
    "date": "2026-09-20",
    "service": "EVENING",
    "time": "20:30",
    "size": 4,
    "zone": "OUTDOOR",
    "leaderName": "Rossi",
    "phone": "+393331234567"
  }'
risposta 201
{
  "hold": {
    "id": "0192c4f1-3a77-7c0e-9b21-6d5a8e4f2b10",
    "date": "2026-09-20",
    "service": "EVENING",
    "time": "20:45",
    "size": 4,
    "status": "HELD",
    "origin": "ONLINE",
    "leaderName": "Rossi",
    "holdExpiresAt": "2026-09-20T18:12:00Z"
  },
  "otpUrl": "https://buked.it/t/osteria-del-mare/otp/9f2b…",
  "shifted": true,
  "message": "All'orario che hai chiesto era pieno: ti abbiamo tenuto il primo posto utile"
}
risposta 409 — nessun posto
{
  "code": "NO_AVAILABILITY",
  "message": "Non c'è posto per 4 persone in questa fascia: guarda le alternative",
  "alternatives": [
    { "time": "21:30", "tableIds": ["01924f…"], "totalSeats": 4 },
    { "time": "22:00", "tableIds": ["01924g…"], "totalSeats": 6 }
  ]
}

Il 409 non è un vicolo cieco: porta con sé le alternative già calcolate. Proponile invece di far ripetere la ricerca.

POST /holds/{id}/confirm con {"token": "…"} conferma (è il token che sta nel link OTP); POST /holds/{id}/cancel con {"token": "…"} rinuncia e libera subito i posti — il token è la prova di possesso: il solo id non basta. Se nessuno conferma, l’hold scade da solo alla data di holdExpiresAt.

La disciplina del telefono

Il numero di telefono è la chiave della prenotazione: è obbligatorio, e un numero garantisce una sola prenotazione viva per serata — chi ha sbagliato prima disdice (si può sempre), poi riprenota. Un numero bloccato dal locale dopo un no-show non prenota finché il locale non lo sblocca. La tua integrazione deve aspettarsi questi rifiuti e mostrarne il rimedio, che è già nel message:

CodiceHTTPCosa significa (e il rimedio)
NUMERO_RICHIESTO400telefono assente o troppo corto per esistere: chiedilo alla persona, mai inventarlo
NUMERO_GIA_IMPEGNATO409quel numero garantisce già una prenotazione della serata: si disdice quella dal suo link, poi si riprenota
NUMERO_BLOCCATO403bloccato dopo un no-show: la strada è la telefonata al locale, non un altro tentativo
CONSENSO_RICHIESTO409c’è posto stringendosi di una sedia: chiedi alla persona e, col suo sì, ripeti la richiesta con "accettaEccedenza": true
DIVISIONE_RICHIESTA409non c’è una tavolata unica ma due tavoli separati sì: col sì della comitiva, ripeti con "accettaDivisione": true
NO_PARKING409con "parking": true il posto auto è un vincolo duro: senza posto libero il tavolo non si tiene — le alternative nel corpo hanno il parcheggio

Per le tavolate oltre le 5 persone la garanzia scala — un numero verificato ogni 5 — ma non si digita nell’hold: la costruisce il link di composizione che la conferma restituisce (compositionUrl). Mentre gli invitati si aggiungono da quel link, ai multipli di 5 il sistema chiede a chi sta facendo l’aggiunta un numero con conferma via SMS; senza conferma l’aggiunta decade. Non c’è niente da implementare lato tuo: gira il link, il resto lo fa la pagina del tavolo.

Lista d’attesa

POST /waitlistbookings:write. Corpo: {date, service, size, name, contact}. Risponde con la voce creata e la posizione in coda. Quando si libera qualcosa, a chi è in lista arriva una finestra esclusiva: l’evento waitlist.offered lo annuncia anche a te.

Eventi

GET /events e GET /events/{id}events:read. Restituiscono gli eventi in vetrina nella forma pubblica: la festa privata di qualcun altro non si sfoglia, esiste solo per chi ha il link diretto.

richiesta
curl -s https://buked.it/api/public/v1/events \
  -H "Authorization: Bearer bk_live_7Qd2mXr9KpZa4B6cD8eF0gHj"
risposta 200 (campi principali)
[
  {
    "id": "0192b7de-1c40-7a3f-8e55-2b91c7d4a601",
    "name": "Cena di San Martino",
    "startsAt": "2026-11-11T20:00:00Z",
    "venueName": "Osteria del Mare",
    "address": "via Sparano 12, Bari",
    "menuOptions": [
      { "id": "carne", "label": "Menù di carne", "active": true },
      { "id": "pesce", "label": "Menù di pesce", "active": true }
    ],
    "allergenOptions": ["glutine", "lattosio", "frutta a guscio"],
    "collecting": true
  }
]

Sala, calendario e report

Sotto /parties, /schedules, /tables e /events/{id}/bookings vivono le viste dell’esercente: comitive del giorno con stato e tavolo, calendario stagionale, pianta della sala, report di cucina. Le rotte sono già instradate e hanno il loro scope assegnato (bookings:read, availability:read, events:read), ma oggi rispondono 403 a una chiave macchina: richiedono ancora la sessione umana della console. Il contratto non cambierà quando si apriranno; nel frattempo, per il lato esercente, la strada praticabile è il server MCP e i webhook.

Webhook

Interrogarci ogni minuto per sapere cos’è cambiato è lo spreco più comune di ogni integrazione — e arriva comunque tardi. Registra un indirizzo e ti chiamiamo noi, firmando ogni consegna.

Iscriversi

POST /webhookswebhooks:manage. In alternativa l’esercente può registrarlo dalla console, senza scrivere una riga. L’indirizzo deve essere https raggiungibile da internet: gli eventi contengono nomi e orari di persone reali.

richiesta
curl -s -X POST https://buked.it/api/public/v1/webhooks \
  -H "Authorization: Bearer bk_live_7Qd2mXr9KpZa4B6cD8eF0gHj" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://gestionale.esempio.it/hook/buked",
    "events": ["party.confirmed", "party.seated", "party.released"]
  }'
risposta 201
{
  "id": "0192c51a-2f88-7b4d-9e10-77c3a2e5b904",
  "url": "https://gestionale.esempio.it/hook/buked",
  "events": ["party.confirmed", "party.seated", "party.released"],
  "active": true,
  "createdAt": "2026-08-04T18:22:41Z",
  "secret": "whsec_2Jm8Qb…"
}

Il secret compare solo qui. Non è una credenziale d'accesso: è la chiave condivisa con cui verifichi che una chiamata arrivi davvero da noi. Per cambiarlo si toglie la sottoscrizione e se ne crea un'altra.

GET /webhooks elenca le sottoscrizioni, DELETE /webhooks/{id} ne toglie una, GET /webhooks/{id}/deliveries restituisce la traccia delle ultime consegne con esito, numero di tentativi e ultimo errore: è la prima risposta a «non ricevo niente».

Tipi di evento

TipoQuando
hold.grantedcapacità messa da parte, in attesa di conferma umana
hold.rejectedrichiesta senza posto (porta le alternative)
hold.expirednessuno ha confermato in tempo: posti liberati
party.confirmedprenotazione confermata: il tavolo è impegnato
party.arrivedla comitiva si è presentata: è in lobby, in fila per l’accompagnamento
party.replannedil piano flessibile l’ha spostata su altri tavoli: la promessa non cambia
party.seatedla comitiva si è seduta — per la cassa, apri il conto
party.closingconto chiesto o servizio finito: il tavolo sta per liberarsi
party.releasedtavolo libero e riassegnabile (anche la disdetta arriva così)
phone.bannedno-show dichiarato dalla sala: bloccati i numeri che garantivano (nel payload, quali)
waitlist.offeredofferta la finestra esclusiva a chi era in coda
availability.changedla disponibilità del giorno è cambiata: invalida le cache (porta anche la stima del prossimo giro di tavoli, campo nextRound)
comanda.closedil tavolo ha chiuso la comanda — per la cassa è un ordine: voci da GET /pos/comande/{partyId}

Forma della consegna

consegna
POST /hook/buked HTTP/1.1
Host: gestionale.esempio.it
Content-Type: application/json
X-Buked-Event: party.seated
X-Buked-Timestamp: 1789234961
X-Buked-Signature: sha256=6b1f0a…e42c

{"type":"party.seated","tenantId":"0192a1…","tenantSlug":"osteria-del-mare",
 "date":"2026-09-20","service":"EVENING",
 "partyId":"0192c4f1-3a77-7c0e-9b21-6d5a8e4f2b10","size":4,"time":"20:45",
 "tableIds":["0192b0…","0192b1…"],"nextRound":"21:40",
 "extra":{},"at":"2026-09-20T18:47:03Z"}

I campi time, tableIds e nextRound ci sono quando hanno senso per quel tipo; extra porta i dettagli specifici (per esempio cause su una chiusura, waitlistId su un’offerta). Programma sui campi che ti servono e ignora gli altri: ne aggiungeremo, senza toglierne.

Verificare la firma

X-Buked-Signature vale sha256= seguito dall’HMAC-SHA256 esadecimale di timestamp + "." + corpo, calcolato con il secret della sottoscrizione. Il timestamp entra dentro la firma, e non è un dettaglio: senza, chi intercetta una consegna autentica potrebbe rispedirla domani cambiandone l’ora e tu non te ne accorgeresti. Con il timestamp firmato puoi scartare tutto ciò che è più vecchio della tua tolleranza — cinque minuti è la scelta consueta.

in pseudocodice
messaggio_firmato = timestamp + "." + corpo_grezzo
firma_attesa     = "sha256=" + hex(HMAC_SHA256(segreto, messaggio_firmato))

accetta se:
  firma_attesa == X-Buked-Signature          (confronto a tempo costante)
  e |adesso - X-Buked-Timestamp| <= 300 s    (anti-replay)
verifica in Node
import crypto from 'node:crypto';
import express from 'express';

const SEGRETO = process.env.BUKED_WEBHOOK_SECRET;   // whsec_…
const TOLLERANZA = 300;                             // secondi

const app = express();

// Il corpo va preso GREZZO. Se lo si fa passare da JSON.parse e poi da
// JSON.stringify i byte cambiano (spazi, ordine delle chiavi) e la firma
// non torna più: si firma quello che è stato spedito, non un equivalente.
app.post('/hook/buked', express.raw({ type: 'application/json' }), (req, res) => {
  const firma = req.get('X-Buked-Signature') || '';
  const ts = Number(req.get('X-Buked-Timestamp'));
  const corpo = req.body.toString('utf8');

  if (!Number.isFinite(ts) || Math.abs(Date.now() / 1000 - ts) > TOLLERANZA) {
    return res.sendStatus(400);   // consegna autentica ma vecchia: non vale
  }

  const attesa =
    'sha256=' +
    crypto.createHmac('sha256', SEGRETO)
          .update(ts + '.' + corpo, 'utf8')
          .digest('hex');

  // timingSafeEqual pretende buffer della stessa lunghezza, e il confronto
  // a tempo costante serve: uno normale esce al primo byte diverso e lascia
  // indovinare la firma un carattere per volta.
  const a = Buffer.from(attesa);
  const b = Buffer.from(firma);
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return res.sendStatus(401);
  }

  const evento = JSON.parse(corpo);
  // Rispondi 2xx SUBITO e lavora dopo: se ci metti troppo ritentiamo, e ti
  // ritrovi lo stesso evento due volte. Deduplica su partyId + type + at.
  res.sendStatus(204);
  void gestisci(evento);
});

Due errori che si fanno quasi sempre: firmare il JSON riserializzato invece dei byte ricevuti (la firma non tornerà mai) e confrontare le firme con === (un confronto che esce al primo byte diverso lascia ricostruire la firma un carattere per volta).

Ritentativi

Consideriamo consegnato solo un 2xx. Altrimenti ritentiamo tre volte, a 1 s, 5 s e 25 s dall’ultimo tentativo: al massimo quattro invii, l’ultimo circa mezzo minuto dopo il fatto. Rispondi presto e lavora dopo — un endpoint lento finisce per ricevere doppioni. Deduplica sull’insieme type + partyId + at.

Profilo per i registratori di cassa

È il pezzo che chiude il cerchio del motore di disponibilità: finché la liberazione di un tavolo dipende da un gesto — il cliente che se ne va, il cameriere che se ne accorge — resta fallibile. Il conto pagato è un fatto che la cassa conosce già: da lì la chiusura diventa automatica e la stima del prossimo giro smette di essere una proiezione.

In buked non c’è nulla di fiscale: lo scontrino resta affare della cassa, noi riceviamo solo il fatto. L’interlocutore reale, del resto, non è il registratore telematico ma il software gestionale che lo pilota.

Mappare i tavoli

Ogni tavolo di buked ha un campo posRef: il numero con cui quel tavolo è conosciuto alla cassa. L’esercente lo compila una volta dalla console, e da lì il dialogo è automatico. La mappatura corrente si legge con GET /pos/tables (pos:write).

richiesta
curl -s https://buked.it/api/public/v1/pos/tables \
  -H "Authorization: Bearer bk_live_7Qd2mXr9KpZa4B6cD8eF0gHj"
risposta 200
{
  "tables": [
    { "posRef": "12", "name": "Tavolo 12", "seats": 6,
      "occupied": true,  "partyId": "0192c4f1…", "covers": 5, "meals": 4 },
    { "posRef": "13", "name": "Dehors 3",  "seats": 4,
      "occupied": false, "partyId": "",         "covers": 0, "meals": 0 }
  ]
}

covers sono le persone sedute, meals i coperti a pasto: sono due numeri diversi, e la differenza arriva fino allo scontrino. Un bambino senza pasto occupa una sedia ma non un coperto.

Da buked verso la cassa

Sottoscrivi party.seated: è il momento in cui aprire il conto sul tavolo giusto. L’evento porta tableIds, size e l’orario; per i coperti già distinti fra posto e pasto interroga GET /pos/tables sul posRef corrispondente. Aggiungi party.released per sapere quando il tavolo è tornato libero anche per vie diverse dal pagamento.

La comanda del tavolo, già battibile

Nei locali dove si ordina per il tavolo intero, gli ospiti dettano la comanda dai loro telefoni e chi ha prenotato la chiude scegliendo come si paga. In quel momento parte comanda.closed (porta partyId, i pezzi e la modalità: PAGO_MANGIO o MANGIO_PAGO); l’ordine intero si legge con GET /pos/comande/{partyId} — le voci come le hanno scritte gli ospiti, il riepilogo già sommato per voce, note e destinatari per il servizio. Le comande chiuse di un giorno si elencano con GET /pos/comande?date=YYYY-MM-DD. Nessun pagamento passa da buked: la comanda è l’ordine, il conto resta della cassa.

Dalla cassa verso buked

POST /pos/tables/{posRef}/bill-paidpos:write. È il segnale di fine partita: la comitiva passa a RELEASED con causa PAYMENT, la regia di sala si aggiorna e a chi è in lista d’attesa parte l’offerta. È idempotente: se il tavolo è già chiuso rispondiamo 200 con alreadyClosed: true, quindi puoi ritrasmettere quanto vuoi.

richiesta
curl -s -X POST \
  https://buked.it/api/public/v1/pos/tables/12/bill-paid \
  -H "Authorization: Bearer bk_live_7Qd2mXr9KpZa4B6cD8eF0gHj" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: scontrino-2026-09-20-0041" \
  -d '{ "billId": "0041", "totalCents": 18600, "currency": "EUR" }'
risposta 200
{
  "party": {
    "id": "0192c4f1-3a77-7c0e-9b21-6d5a8e4f2b10",
    "status": "RELEASED",
    "closeCause": "PAYMENT",
    "time": "20:45",
    "size": 5
  },
  "released": true
}

POST /pos/tables/{posRef}/bill-opened è facoltativo e conferma che il tavolo è davvero occupato: una seduta osservata invece che dedotta. Se sul numero di cassa non c’è nessuna comitiva attiva rispondiamo 404 NO_ACTIVE_PARTY.

Server MCP

Sempre più spesso chi prenota non apre un sito: chiede a un assistente. E sempre più spesso il ristoratore, invece di leggere un report, domanda «quanti celiaci stasera?». Il server MCP (Model Context Protocol) di buked esiste per questo: dà agli agenti AI un modo strutturato di parlare con la piattaforma, con gli stessi permessi e la stessa tracciabilità di qualunque altra integrazione.

Collegarsi

Endpoint: https://buked.it/mcp, JSON-RPC 2.0 su POST, revisione del protocollo 2025-06-18. L’autenticazione è la stessa API key. Metodi: initialize, tools/list, tools/call, ping.

configurazione del client MCP
{
  "mcpServers": {
    "buked": {
      "type": "http",
      "url": "https://buked.it/mcp",
      "headers": {
        "Authorization": "Bearer bk_live_7Qd2mXr9KpZa4B6cD8eF0gHj"
      }
    }
  }
}

È la forma che usano i client MCP più diffusi per un server remoto; adatta i nomi dei campi al tuo.

una chiamata a mano
curl -s -X POST https://buked.it/mcp \
  -H "Authorization: Bearer bk_live_7Qd2mXr9KpZa4B6cD8eF0gHj" \
  -H "Content-Type: application/json" \
  -H "MCP-Protocol-Version: 2025-06-18" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
      "name": "cerca_disponibilita",
      "arguments": {
        "giorno": "2026-09-20",
        "servizio": "EVENING",
        "persone": 4
      }
    }
  }'

Gli strumenti

tools/list mostra solo gli strumenti che la tua chiave può davvero usare: i due profili — lato ospite e lato esercente — non sono scritti a mano, si deducono dagli scope. Le descrizioni sono in italiano, pensate per essere lette da un modello.

StrumentoCosa faScope
cerca_disponibilitaorari prenotabili di un giorno e una fasciaavailability:read
prossimo_roundquando è previsto il prossimo giro di tavoliavailability:read
richiedi_holdchiede una pre-prenotazione (scrive)bookings:write
stato_salafotografia della sala: comitive, tavoli, presentibookings:read
tavolo_walkinapre un tavolo per chi si presenta di persona (scrive)bookings:write
elenca_prenotazioninuclei di un evento con presenti e coperti a pastobookings:read
report_cucinacoperti per categoria, preferenze, allergiebookings:read

L’agente prepara, l’umano conferma

È il principio che regge tutta la superficie AI, e non è una cautela di facciata: un agente può ottenere un hold, ma la conferma resta il link OTP che arriva sul telefono della persona. L’anti-abuso del motore non si aggira passando da MCP — vale per le intelligenze artificiali esattamente quanto per i bot. Un assistente che promette un tavolo che il tool non ha confermato sta mentendo al proprio utente; le descrizioni degli strumenti lo dicono esplicitamente al modello, e vale la pena ripeterlo anche nel prompt della tua applicazione.

Limiti e buone maniere

  • 20 chiavi attive e 20 sottoscrizioni webhook per attività, fino a 30 tipi di evento per sottoscrizione. Le chiavi si revocano, non si accumulano: un elenco che nessuno riesce più a leggere è un problema di sicurezza.
  • Una chiave, un’attività. Se integri più esercenti, tieni una chiave per ciascuno: è anche l’unico modo perché ognuno possa revocare la propria.
  • Chiedi in modo civile. La disponibilità cambia di continuo, ma non serve interrogarla in ciclo: usa i webhook per sapere quando qualcosa è cambiato e chiedi il dettaglio solo allora. 429 e 503 vanno trattati come ritentabili, con attesa crescente.
  • Dati di persone. Nomi, orari ed esigenze alimentari sono dati personali, e le esigenze alimentari sono dati sanitari: viaggiano perché servono a portare il piatto giusto alla persona giusta, non per altro. Ogni consegna webhook è registrata — chi riceve dati degli ospiti, e quando, è informazione di conformità che l’esercente può consultare.
  • Revoca senza preavviso. Un esercente può revocare una chiave in qualunque momento: gestisci il 401 come uno stato normale e dillo chiaramente al tuo utente, invece di ritentare in silenzio.

Qualcosa non torna o manca una rotta che ti servirebbe? Scrivi a developers@buked.it: la superficie pubblica cresce con i casi d’uso reali, non con quelli immaginati.