Pubblicare su Bluesky richiede tre chiamate: com.atproto.server.createSession per ottenere i token, com.atproto.repo.uploadBlob se hai immagini e com.atproto.repo.createRecord per scrivere un record app.bsky.feed.post. Non c’è revisione dell’app né schermata di consenso OAuth da far approvare. Il prezzo di questa semplicità è che link e menzioni non vengono rilevati automaticamente: gli offset in byte su UTF-8 li calcoli tu.
Cosa può pubblicare l’API di Bluesky, e cosa no?
Un record di post è un semplice documento JSON. I campi obbligatori sono $type (app.bsky.feed.post), text e createdAt come timestamp ISO 8601. Tutto il resto è struttura opzionale.
| Funzione | Supporto |
|---|---|
| Immagini per post | 4 al massimo |
| Dimensione immagine | 1.000.000 byte ciascuna, secondo la documentazione del post |
| Dimensione totale blob per post | 2.000.000 byte al massimo |
| Testo alternativo | Obbligatorio per immagine, con un aspect ratio |
| Post citati | app.bsky.embed.record |
| Card dei link | app.bsky.embed.external, con una miniatura tua |
| Risposte | reply con riferimenti forti root e parent |
| Tag di lingua | langs, un array come ["en-US"] |
Le lacune da segnalare subito:
- Niente è rilevato automaticamente. Link, menzioni e hashtag sono testo inerte a meno che tu non alleghi i facet. Un URL incollato senza facet non è cliccabile.
- Le card dei link sono compito tuo.
app.bsky.embed.externalrichiede titolo, descrizione e miniatura. Nulla estrae questi dati dalla pagina per te. - Le menzioni richiedono un DID risolto. Non puoi mettere un handle in un facet: prima risolvi l’handle in un DID.
- Dalle immagini va rimosso l’EXIF prima del caricamento, secondo la documentazione.
- La documentazione del post dichiara che non c’è un limite di caratteri o grafemi. Non siamo riusciti a verificarne uno da quella pagina, quindi non citiamo un numero. Vedi il post sul limite di caratteri di Bluesky per quanto applicato dal client.
Nota: i dati qui riportati sono stati verificati sulla documentazione sviluppatori di Bluesky (docs.bsky.app, che ora reindirizza a bsky.network/docs) a settembre 2026. Le piattaforme li cambiano senza preavviso.
Qual è il modello di autenticazione?
Questa è la sezione sull’autenticazione più breve che leggerai per qualsiasi piattaforma social.
Non c’è nessuna registrazione dell’app, nessuna revisione dell’app e nessuna schermata di consenso OAuth per il percorso con app password. Un utente crea una app password nelle proprie impostazioni Bluesky e la consegna al tuo software. Scambi l’handle più quella app password per un JWT di accesso e un JWT di refresh su com.atproto.server.createSession. I token di accesso hanno vita breve; li rinnovi con il JWT di refresh.
Due conseguenze. Una app password è una credenziale che l’utente ti consegna direttamente: nessuna schermata di consenso significa nessuna concessione di scope mediata dalla piattaforma, quindi l’obbligo di conservarla ricade interamente su di te. Cifrale e dai agli utenti un modo visibile per disconnettersi.
E la rete non appartiene a un’unica azienda. Gli account AT Protocol vivono su un Personal Data Server, e un PDS è auto-ospitabile. Il tuo client parla con l’host PDS dell’utente, quindi hardcodare bsky.social funziona oggi ma non è il modello del protocollo. Leggi l’host dal documento DID dell’utente.
Come si pubblica davvero? La sequenza di chiamate
POST /xrpc/com.atproto.server.createSessionconidentifier(handle o DID) epassword(la app password). RestituisceaccessJwt,refreshJwtedid.- Se hai immagini:
POST /xrpc/com.atproto.repo.uploadBlobper ogni immagine, con i byte grezzi e ilContent-Typecorretto. Ognuna restituisce un riferimento al blob. - Calcola i facet per link o menzioni, come offset in byte sulla codifica UTF-8 di
text. POST /xrpc/com.atproto.repo.createRecordconrepoimpostato sul tuo DID,collectionimpostato suapp.bsky.feed.poste il record stesso.
const base = 'https://bsky.social/xrpc';
const auth = await fetch(`${base}/com.atproto.server.createSession`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identifier: handle, password: appPassword }),
}).then((r) => r.json());
const text = 'Notes on the release: https://example.com/changelog';
const url = 'https://example.com/changelog';
const enc = new TextEncoder();
const byteStart = enc.encode(text.slice(0, text.indexOf(url))).length;
await fetch(`${base}/com.atproto.repo.createRecord`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${auth.accessJwt}`,
},
body: JSON.stringify({
repo: auth.did,
collection: 'app.bsky.feed.post',
record: {
$type: 'app.bsky.feed.post',
text,
createdAt: new Date().toISOString(),
facets: [
{
index: { byteStart, byteEnd: byteStart + enc.encode(url).length },
features: [{ $type: 'app.bsky.richtext.facet#link', uri: url }],
},
],
},
}),
});
L’aritmetica dei facet, spiegata
byteStart e byteEnd sono offset nei byte UTF-8 del testo, non negli indici di stringa JavaScript e non nei caratteri.
"café".length vale 4 in JavaScript, ma la codifica UTF-8 è di 5 byte. Qualsiasi emoji prima del tuo link sposta l’offset di 4 byte spostando l’indice di stringa di 2. Se sbagli qui, il link viene reso come testo rotto o evidenzia lo span sbagliato, senza alcun errore dal server: il record è valido, punta solo ai byte sbagliati.
Codifica la stringa una volta, trova gli offset nell’array di byte e non mescolare mai i due sistemi di coordinate. La feature del facet per il link usa uri, non url.
Quali sono i limiti di frequenza di Bluesky?
Le scritture sono misurate con un sistema a punti per account: CREATE costa 3 punti, UPDATE 2, DELETE 1, contro un budget di 5.000 punti all’ora e 35.000 al giorno. Questo equivale a circa 1.666 creazioni all’ora e 11.666 al giorno.
| Limite | Valore |
|---|---|
| Punti di scrittura | 5.000/ora, 35.000/giorno |
| Creazioni (derivate) | ~1.666/ora, ~11.666/giorno |
| Richieste PDS totali | 3.000 ogni 5 minuti, per IP |
createSession | 30 ogni 5 minuti, 300 al giorno, per account |
| Tetto per il caricamento blob | 52.428.800 byte (50 MB) |
Il limite di createSession è quello che coglie di sorpresa gli strumenti di programmazione. 30 ogni 5 minuti per account significa che devi mettere in cache la sessione e rinnovarla, invece di autenticarti a ogni post. Un processo che crea una sessione a ogni operazione si autolimita ben prima di raggiungere qualsiasi limite di pubblicazione.
Nota le due cifre sui blob: il PDS accetta blob fino a 50 MB, mentre la documentazione del post dichiara un limite di 1.000.000 byte per immagine e 2.000.000 byte totali per le immagini del post. Dimensiona in base al più piccolo.
Nota: i dati qui riportati sono stati verificati sulla documentazione sui limiti di frequenza di Bluesky a settembre 2026. Le piattaforme li cambiano senza preavviso.
Cosa ti costerà davvero tre settimane?
Bluesky è davvero la piattaforma più economica da integrare tra quelle principali, ma “economica” non vuol dire “gratis”.
Calcolo dei facet su testo reale. Rilevare URL, punteggiatura finale, handle e hashtag, convertire ogni corrispondenza in offset di byte UTF-8 e mantenere tutto corretto quando l’utente modifica il testo. È qui che vivono i bug.
Gestione del budget dei blob. 2 MB totali su un massimo di 4 immagini significa ridimensionare e ricodificare lato server, rimuovere l’EXIF e decidere cosa fare quando le foto dell’utente non ci stanno.
Card dei link. Per far sembrare i post dei post veri, recuperi la pagina di destinazione, ne estrai titolo, descrizione e immagine, carichi quell’immagine come blob e costruisci l’embed external. È un piccolo crawler con timeout e gestione degli errori.
Cache della sessione, per via del tetto di 30 ogni 5 minuti, e risoluzione dell’host PDS, perché assumere bsky.social rompe gli account auto-ospitati in un modo che non puoi correggere dal tuo lato.
Se stai rispecchiando contenuti in entrata, il cross-posting da X a Bluesky e il cross-posting da Threads a Bluesky coprono entrambi le differenze di lunghezza del testo e dei media da conciliare prima ancora che il record sia valido.
In breve
- Tre chiamate:
createSession,uploadBlobper le immagini,createRecordcon un recordapp.bsky.feed.post. - Nessuna revisione dell’app, nessuna schermata OAuth. App password, consegnate dall’utente.
- Link e menzioni richiedono facet con offset in byte su UTF-8. Niente è rilevato automaticamente.
- 4 immagini per post, 1.000.000 byte ciascuna, 2.000.000 byte totali, testo alternativo obbligatorio.
- Le scritture costano 3 punti per creazione contro 5.000/ora e 35.000/giorno.
createSessionè 30 ogni 5 minuti. - Gli account possono vivere su un PDS auto-ospitato. Non hardcodare l’host.
Pubblicare su Bluesky insieme ad altri 14 network
Bluesky è quello facile. Lo stesso prodotto di solito richiede anche X (OAuth 2.0 PKCE, caricamento media a blocchi, fatturazione per richiesta), Threads (revisione dell’app Meta, container e poi pubblicazione, refresh del token ogni 60 giorni) e TikTok (una revisione della Content Posting API prima ancora di poter pubblicare). Ognuno ha la propria autenticazione, la propria pipeline media e il proprio modello di fallimento asincrono.
BulkPublish è un’unica API REST su 15 piattaforme, Bluesky inclusa, con calcolo dei facet, ridimensionamento dei blob e refresh delle sessioni gestiti lato server. La documentazione per sviluppatori e il riferimento API REST contengono gli endpoint, e programmare post su Bluesky copre lo stesso argomento senza codice.