API di Bluesky: come pubblicare da codice (2026)

API di Bluesky: come pubblicare da codice (2026)

Crea una sessione, carica un blob, scrivi un record app.bsky.feed.post. Nessuna revisione dell’app, nessuna schermata OAuth, ma i facet dei link li calcoli tu.

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.

FunzioneSupporto
Immagini per post4 al massimo
Dimensione immagine1.000.000 byte ciascuna, secondo la documentazione del post
Dimensione totale blob per post2.000.000 byte al massimo
Testo alternativoObbligatorio per immagine, con un aspect ratio
Post citatiapp.bsky.embed.record
Card dei linkapp.bsky.embed.external, con una miniatura tua
Rispostereply con riferimenti forti root e parent
Tag di lingualangs, 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.external richiede 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

  1. POST /xrpc/com.atproto.server.createSession con identifier (handle o DID) e password (la app password). Restituisce accessJwt, refreshJwt e did.
  2. Se hai immagini: POST /xrpc/com.atproto.repo.uploadBlob per ogni immagine, con i byte grezzi e il Content-Type corretto. Ognuna restituisce un riferimento al blob.
  3. Calcola i facet per link o menzioni, come offset in byte sulla codifica UTF-8 di text.
  4. POST /xrpc/com.atproto.repo.createRecord con repo impostato sul tuo DID, collection impostato su app.bsky.feed.post e 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.

LimiteValore
Punti di scrittura5.000/ora, 35.000/giorno
Creazioni (derivate)~1.666/ora, ~11.666/giorno
Richieste PDS totali3.000 ogni 5 minuti, per IP
createSession30 ogni 5 minuti, 300 al giorno, per account
Tetto per il caricamento blob52.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, uploadBlob per le immagini, createRecord con un record app.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.

Correlati