L’API pubblica
Blupinta ha un’API pubblica per le altre app, come una dashboard di casa che mostra l’ultima versione salvata di una casa. Sta su api.blupinta.app, e il suo indirizzo da solo dice cos’è:
GET https://api.blupinta.app/{ "name": "Blupinta API", "version": "v1", "resource": "https://api.blupinta.app/v1", "issuer": "https://auth.blupinta.app", "discovery": "https://auth.blupinta.app/.well-known/openid-configuration", "scopes": { "projects:read": "the saved versions of the one project the person chose" }, "endpoints": ["GET /v1/projects/{projectId}/home", "GET /v1/projects/{projectId}/revisions", "GET /v1/projects/{projectId}/revisions/{number}"], "docs": "https://docs.blupinta.app/reference/public-api/"}La tua app
Sezione intitolata “La tua app”Un’app ha bisogno di un client id. Un dispositivo può cominciare subito con client_id=device, senza
registrarsi: usa il device flow, e la persona gli dà un nome nella pagina del consenso (Tablet cucina), con cui lo
ritrova tra le app collegate. Un’app non tiene mai un nostro segreto: un dispositivo o una pagina è un client pubblico
e si fa riconoscere con PKCE, un server firma con una chiave sua.
Registrarla
Sezione intitolata “Registrarla”Ogni altra app si registra da Impostazioni › Sviluppatori, una sezione che vedono gli account abilitati a creare app (per ora su richiesta). Registra un’app chiede:
- il suo nome, quello che le persone leggono quando la collegano;
- come accede: una pagina web (nel browser, PKCE, torna a un indirizzo
https), un’app installata (PKCE, torna con il suo schema,com.example.app:/callback, o ahttp://127.0.0.1), un dispositivo (il device flow, nessun indirizzo) o un server (si firma con la sua chiave privata,private_key_jwt); - i suoi indirizzi di ritorno, uno per riga;
httpsolo versolocalhost, mentre la sviluppi; - le chiavi pubbliche di un server, come JWKS (
{ "keys": [ … ] }): la chiave privata non lascia mai il tuo server; - cosa può chiedere, gli scope che può domandare.
Quello che non può funzionare viene rifiutato con il motivo, come lo dice l’API. Ogni app ha il suo client_id, da Copiare; un account ne registra fino a dieci. Un’app nuova è non verificata: puoi collegarla solo tu, e la sua pagina del consenso lo dice, finché Blupinta non verifica chi la fa (scrivici). Modifica la cambia per il prossimo accesso; Elimina chiede prima, poi ferma subito il suo client_id e scollega chi l’aveva collegata.
Le librerie di Blupinta (TypeScript, React, la riga di comando, un server MCP, Python) fanno l’accesso e le chiamate per te.
Ottenere un token
Sezione intitolata “Ottenere un token”I token arrivano da auth.blupinta.app, un provider OpenID Connect: il suo documento di discovery elenca ogni indirizzo. La persona entra lì e consente la tua app, scegliendo l’unico progetto che può leggere (Collegare un’app).
- Un dispositivo (senza tastiera, come un tablet a parete) usa il device flow:
POST /oidc/device/authcon il tuoclient_idescope=openid offline_access projects:read; mostra alla personauser_codeeverification_uri; chiediPOST /oidc/tokencon ildevice_codeogniintervalsecondi finché risponde. - Una pagina o un’app con un browser usa l’authorization code con PKCE (
S256), mandando la persona su/oidc/authorizee scambiando il codice su/oidc/token.
Chiedi offline_access per restare collegata: ricevi anche un refresh token, che cambia a ogni uso e dura 90 giorni
dall’ultimo. Un access token dura un’ora. Il token vale per la risorsa https://api.blupinta.app/v1.
Leggere
Sezione intitolata “Leggere”Ogni richiesta porta il token: Authorization: Bearer …. I progetti sono quelli che la persona ha scelto; GET /v1/projects li elenca.
| richiesta | risposta |
|---|---|
GET /v1/projects |
i progetti che l’app può leggere, per nome, ognuno con numero e data della sua ultima versione |
GET /v1/projects/{projectId} |
uno di loro |
GET /v1/projects/{projectId}/home |
l’ultima versione salvata: numero, messaggio, autore, data e la casa stessa (il formato di Blupinta, vedi Il file della casa) |
GET /v1/projects/{projectId}/revisions |
tutte le versioni salvate, dalla più recente, senza la casa, in rows; next dice dove comincia la pagina dopo, null sull’ultima (oggi tutte le versioni stanno in una) |
GET /v1/projects/{projectId}/revisions/{number} |
una versione, con la sua casa |
GET /v1/projects/{projectId}/files/{sha256} |
un file che una versione nomina con il suo sha (un disegno, l’immagine di un pavimento): un rimando al file, firmato per pochi minuti; i file non cambiano mai, tienili |
GET /v1/projects/{projectId}/catalog/models/{sha256}, …/thumbnails/{sha256} |
il modello 3D o la miniatura di un prodotto che i products della casa nominano, allo stesso modo |
GET /v1/projects/{projectId}/thumbnail |
l’immagine dell’ultima versione, la sua prima pianta vista dall’alto (WebP, 640 × 400), allo stesso modo dei file; 404 se quella versione non ce l’ha |
GET /v1/projects/{projectId}/export |
uno zip di tutte le versioni e dei file che nominano |
GET /v1/projects/{projectId}/events |
un flusso di eventi (server-sent events) che resta aperto: revision.saved con il number della nuova versione quando ne viene salvata una; access.revoked (app scollegata, o progetto tolto) o token.expired prima che finisca |
Con gli eventi un’app non deve chiedere di continuo: legge la casa quando le viene detto che è stata salvata una
versione. Dopo token.expired rinnova il token e riapre il flusso; dopo access.revoked si ferma.
Scrivere
Sezione intitolata “Scrivere”Un’app a cui è consentito projects:write lavora sui progetti che la persona ha scelto; una con projects:create ne crea
di nuovi nel suo account (e poi può leggerli e scriverli). Scrive come la persona che l’ha collegata, quindi solo sui
progetti che lei può modificare, mai su quelli che può solo guardare (403).
| richiesta | cosa fa |
|---|---|
POST /v1/projects con { "name": …, "address": … } |
un progetto nuovo, con la casa vuota; 201 con il progetto |
PATCH /v1/projects/{projectId} con { "name": …, "address": … } |
lo rinomina |
POST /v1/projects/{projectId}/files?name=pianta.png, i byte del file come corpo e il suo tipo come content-type |
un disegno o un’immagine per la casa (PNG, JPEG, WebP, PDF); il suo sha256 è il modo in cui la casa lo nomina; gli stessi byte di nuovo rispondono 200 |
POST /v1/projects/{projectId}/revisions con { "home": …, "message": …, "baseRevision": 3 } |
salva la casa come versione successiva, con gli stessi controlli di Crea versione nell’editor; baseRevision è l’ultima versione che l’app aveva (null se non ce n’erano): se nel frattempo ne è stata salvata un’altra risponde 409 conflict, così il lavoro di nessuno viene sovrascritto |
Una versione salvata così lascia com’è la bozza dell’editor; la cronologia del progetto dice quale app l’ha salvata.
Manda un header Idempotency-Key con una scrittura e la stessa richiesta rimandata (dopo una risposta persa) riceve la
stessa risposta, eseguita una volta sola.
Webhook
Sezione intitolata “Webhook”Un’app a cui è consentito webhooks:manage che non può tenere aperto il flusso degli eventi (un server, un’automazione)
dà a Blupinta un suo indirizzo: POST /v1/webhooks con { "url": "https://…", "events": ["revision.saved", "access.revoked"] }. La risposta contiene il secret del webhook, mostrato solo questa volta. Da allora Blupinta
manda a quell’indirizzo ogni evento dei progetti che l’app raggiunge, in JSON ({ "id", "type", "createdAt", "data": { "projectId", "number" } }), con tre header: Blupinta-Event, Blupinta-Delivery (lo stesso id a ogni nuovo tentativo:
un’app che l’ha già visto può saltarlo) e Blupinta-Signature: t=<secondi>,v1=<hex>. Controlla la firma prima di fidarti
del corpo:
const [, t, v1] = /^t=(\d+),v1=([0-9a-f]+)$/.exec(signature)const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')const isGenuine = timingSafeEqual(Buffer.from(v1, 'hex'), Buffer.from(expected, 'hex')) && Date.now() / 1000 - t < 300Rispondi 2xx entro 10 secondi. Qualsiasi altra risposta viene ritentata dopo 1 minuto, 5 minuti, 30 minuti, 2 ore e
12 ore; dopo tre giorni di soli fallimenti il webhook viene spento, e GET /v1/webhooks dice quando e perché. GET /v1/webhooks/{webhookId}/deliveries elenca gli ultimi invii con quello che l’app ha risposto; DELETE /v1/webhooks/{webhookId} lo elimina. L’indirizzo deve essere HTTPS e su internet: Blupinta non manda mai niente dentro
una rete privata. Un’app scollegata perde i suoi webhook con lei.
Commenti
Sezione intitolata “Commenti”Un’app a cui è consentito comments:read legge le discussioni appuntate sulle piante dei progetti (GET /v1/projects/{projectId}/threads); una con comments:write ne apre una (POST …/threads con { "planId", "anchor": { "ref": { "kind": "wall", "id": "north" }, "at": [120, 40] }, "body" }), risponde (POST …/threads/{threadId}/comments
con { "body" }) e la risolve o la riapre (PATCH …/threads/{threadId} con { "isResolved": true }). Scrive come la
persona che l’ha collegata, chiunque possa guardare il progetto può commentare, e ogni messaggio dice che l’ha scritto
l’app. Le menzioni (@qualcuno) restano nell’editor.
Il catalogo
Sezione intitolata “Il catalogo”Un’app a cui è consentito catalog:read cerca nel catalogo dei prodotti di Blupinta come fa l’editor: GET /v1/catalog/products?q=libreria&brand=IKEA&mount=floor&sort=name&language=it, a pagine (limit, al massimo 100, e
after, il next della pagina prima), con quanti ne ha trovati in tutto. GET /v1/catalog/products/{namespace}/{name}
(ikea/kallax) legge un prodotto per intero, come una casa lo tiene tra i suoi products; GET /v1/catalog/models/{sha256} e …/thumbnails/{sha256} i suoi file.
Controllare il file di una casa
Sezione intitolata “Controllare il file di una casa”POST /v1/homes/check con { "home": … }, il contenuto di un .home.json di qualsiasi versione, risponde cosa ne
dicono i controlli di Blupinta: readAs (la versione che il file dichiara), checkedAt (quella attuale: il file ci viene
portato prima, come farebbe un’importazione), isValid, e i suoi problems. Un file che non è una casa ha problemi
format, ognuno con il punto in cui è; una casa ha i problemi che le impedirebbero di essere salvata come versione
(area-without-rooms, open-room, crossing…), con la pianta e gli elementi che riguardano. Non viene tenuto nulla.
Cosa serve a una dashboard di casa
Sezione intitolata “Cosa serve a una dashboard di casa”Un’app a cui è consentito hosts:manage (chiedilo insieme a projects:read; il client device di Blupinta può)
dice a Blupinta su cosa conta il suo codice, progetto per progetto:
| richiesta | cosa fa |
|---|---|
PUT /v1/projects/{projectId}/host-references con { "refs": [{ "kind": "wall", "id": "north" }] } |
gli elementi della casa che l’app nomina (tipi: plan, level, area, presentation, room, wall, opening, item, light, awning, plate, stair), ognuno presente nell’ultima versione salvata; da allora una versione che ne toglie uno non si può salvare, e l’editor dice quale app ne ha bisogno |
PUT /v1/projects/{projectId}/devices con { "devices": [{ "id": "light.soggiorno", "name": "Soggiorno", "kind": "light", "room": "Sala" }] } |
i dispositivi dell’app (light, cover, switch, sensor, climate, media), proposti nell’editor quando colleghi luci e tapparelle della casa |
GET su entrambi |
quello che l’app ha dichiarato |
Ogni PUT sostituisce l’intera lista; una sbagliata risponde 400 con { "error": "invalid_input", "problems": [...] }.
Il nome e l’indirizzo email della persona non fanno parte di /v1: chiedi profile ed email quando colleghi l’app e
leggili, come fa OpenID Connect, dallo userinfo_endpoint del documento di discovery
(https://auth.blupinta.app/oidc/userinfo). Un’app legge come la persona che l’ha
collegata: un progetto che lei lascia è chiuso anche all’app.
L’ultima versione porta il suo ETag: rimandalo in If-None-Match e una casa che non è cambiata risponde 304,
senza niente da scaricare. Una pagina di qualsiasi sito può chiamare l’API: il token viaggia in un header, mai in un
cookie, e le risposte si lasciano leggere da ogni origine.
Ogni operazione, con parametri, risposte e rifiuti, è nel riferimento dell’API (in inglese),
generato dal documento OpenAPI che l’API serve a GET https://api.blupinta.app/v1/openapi.json: dallo a un
generatore di client, o ai tuoi strumenti, per chiamare l’API da qualsiasi linguaggio.
Quando dice di no
Sezione intitolata “Quando dice di no”- 401: nessun token, uno scaduto o revocato, o un’app che la persona ha scollegato. Rinnova il token; se fallisce anche quello, l’app va ricollegata.
- 403: il token non ha lo scope che la richiesta chiede.
- 404: un progetto diverso da quello consentito, o uno che non esiste: l’API non dice quale dei due.
- 429: troppe richieste: aspetta quanto dice
Retry-After(secondi). - 500: qualcosa non è andato dalla parte di Blupinta; quando chiedi aiuto, cita l’
errorIddella risposta.
Ogni rifiuto è { "error": "<codice>" }, e il codice dice quale di questi è.
Ogni risposta dice quante richieste la tua app può ancora fare: RateLimit-Limit (600 al minuto per ogni
collegamento della tua app, tutti i suoi token contati insieme), RateLimit-Remaining e RateLimit-Reset (i secondi
che mancano al minuto successivo). Rallenta quando RateLimit-Remaining si avvicina a 0, invece di aspettare il 429.
Modifiche
Sezione intitolata “Modifiche”/v1 cresce soltanto: possono arrivare un campo nuovo, un’operazione nuova, un parametro facoltativo nuovo, quindi
leggi quello che ti serve e ignora quello che non conosci. Un cambiamento che risponderebbe diversamente arriva come
/v2.
Un’operazione destinata a sparire è segnata deprecated nel documento OpenAPI,
e ogni sua risposta porta Deprecation (da quando, RFC 9745), Sunset (il giorno in cui smette di rispondere, almeno
sei mesi dopo, RFC 8594) e un Link a questa pagina. Ognuna è elencata qui sotto con quello che la sostituisce.
Oggi nessuna è deprecata.