Bluesky API: hoe je post vanuit code (2026)

Bluesky API: hoe je post vanuit code (2026)

Maak een sessie aan, upload een blob, schrijf een app.bsky.feed.post record. Geen app-review, geen OAuth-scherm, maar je berekent linkfacetten zelf.

Posten naar Bluesky vergt drie aanroepen: com.atproto.server.createSession om tokens te krijgen, com.atproto.repo.uploadBlob als je afbeeldingen hebt, en com.atproto.repo.createRecord om een app.bsky.feed.post record te schrijven. Er is geen app-review en geen OAuth-toestemmingsscherm om goedgekeurd te krijgen. De prijs van die eenvoud is dat links en vermeldingen niet automatisch worden gedetecteerd: je berekent zelf byte-offsets over UTF-8.

Wat kan de Bluesky API publiceren, en wat niet?

Een post record is een simpel JSON-document. De verplichte velden zijn $type (app.bsky.feed.post), text en createdAt als een ISO 8601-tijdstempel. Al het overige is optionele structuur.

FunctieOndersteuning
Afbeeldingen per postmaximaal 4
Afbeeldingsgrootteelk 1.000.000 bytes, volgens de post-documentatie
Totale blobgrootte per postmaximaal 2.000.000 bytes
Alt-tekstverplicht per afbeelding, met een beeldverhouding
Quote postsapp.bsky.embed.record
Linkkaartenapp.bsky.embed.external, met je eigen thumbnail
Reactiesreply met root en parent sterke referenties
Taaltagslangs, een array zoals ["en-US"]

De beperkingen die je vooraf moet weten:

  • Niets wordt automatisch gedetecteerd. Links, vermeldingen en hashtags zijn inerte tekst tenzij je facetten toevoegt. Een URL die je plakt zonder facet is niet klikbaar.
  • Linkkaarten zijn jouw taak. app.bsky.embed.external wil de titel, beschrijving en thumbnail. Niets scraapt de pagina voor je.
  • Vermeldingen hebben een opgeloste DID nodig. Je kunt geen handle in een facet zetten; je lost de handle eerst op naar een DID.
  • Afbeeldingen moeten voor upload van EXIF worden ontdaan, volgens de documentatie.
  • De post-documentatie vermeldt geen limiet op tekens of grafemen. We konden geen cijfer verifiëren op die pagina, dus we noemen er geen. Zie de Bluesky tekenlimiet post voor wat de client afdwingt.

Let op: cijfers hier zijn geverifieerd tegen de Bluesky developer-documentatie (docs.bsky.app, die nu doorverwijst naar bsky.network/docs) per september 2026. Platforms wijzigen dit zonder aankondiging.

Hoe ziet het authenticatiemodel eruit?

Dit is de kortste authenticatiesectie die je voor welk social platform dan ook zult lezen.

Er is geen app-registratie, geen app-review en geen OAuth-toestemmingsscherm voor het pad met app-wachtwoord. Een gebruiker maakt een app-wachtwoord aan in de Bluesky-instellingen en geeft dit aan jouw software. Je wisselt de handle plus dat app-wachtwoord in voor een access JWT en een refresh JWT bij com.atproto.server.createSession. Access tokens hebben een korte levensduur; je vernieuwt ze met de refresh JWT.

Twee gevolgen. Een app-wachtwoord is een inlogmiddel dat je gebruiker rechtstreeks aan je overhandigt: geen toestemmingsscherm betekent geen door het platform bemiddelde scope-toekenning, dus de opslagplicht ligt volledig bij jou. Versleutel ze, en geef gebruikers een zichtbare manier om de verbinding te verbreken.

En het netwerk is niet eigendom van één bedrijf. AT Protocol-accounts wonen op een Personal Data Server, en een PDS is zelf te hosten. Jouw client praat met de PDS-host van de gebruiker, dus bsky.social hardcoderen werkt vandaag, maar is niet het model van het protocol. Lees de host uit het DID-document van de gebruiker.

Hoe post je daadwerkelijk? De aanroepvolgorde

  1. POST /xrpc/com.atproto.server.createSession met identifier (handle of DID) en password (het app-wachtwoord). Geeft accessJwt, refreshJwt en did terug.
  2. Als je afbeeldingen hebt: POST /xrpc/com.atproto.repo.uploadBlob per afbeelding, met de ruwe bytes en de juiste Content-Type. Elke aanroep geeft een blobreferentie terug.
  3. Bereken facetten voor eventuele links of vermeldingen, als byte-offsets in de UTF-8-codering van text.
  4. POST /xrpc/com.atproto.repo.createRecord met repo ingesteld op je DID, collection ingesteld op app.bsky.feed.post, en het record zelf.
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 }],
        },
      ],
    },
  }),
});

De facetrekensom, uitgelegd

byteStart en byteEnd zijn offsets in de UTF-8 bytes van de tekst, niet in JavaScript-stringindices en niet in tekens.

"café".length is 4 in JavaScript, maar de UTF-8-codering is 5 bytes. Elke emoji vóór je link verschuift de offset met 4 bytes terwijl de stringindex slechts met 2 verschuift. Doe je dit fout, dan wordt de link weergegeven als kapotte tekst of markeert hij het verkeerde stuk, zonder foutmelding van de server: het record is geldig, het wijst alleen naar de verkeerde bytes.

Codeer de string één keer, zoek offsets op in de bytearray, en meng de twee coördinatensystemen nooit. De linkfacet-functie gebruikt uri, niet url.

Wat zijn de Bluesky rate limits?

Schrijfacties worden gemeten met een puntensysteem per account: CREATE kost 3 punten, UPDATE 2, DELETE 1, tegen een budget van 5.000 punten per uur en 35.000 per dag. Dat komt neer op ruwweg 1.666 creates per uur en 11.666 per dag.

LimietWaarde
Schrijfpunten5.000/uur, 35.000/dag
Creates (afgeleid)~1.666/uur, ~11.666/dag
Totaal PDS-verzoeken3.000 per 5 minuten, per IP
createSession30 per 5 minuten, 300 per dag, per account
Bloblimiet bij upload52.428.800 bytes (50 MB)

De createSession-limiet is degene die schedulers verrast. 30 per 5 minuten per account betekent dat je de sessie cachet en vernieuwt, in plaats van per post in te loggen. Een worker die bij elke taak een sessie aanmaakt, beperkt zichzelf ruim voordat die enige postlimiet raakt.

Let op de twee blobcijfers: de PDS accepteert blobs tot 50 MB, terwijl de post-documentatie een limiet van 1.000.000 bytes per afbeelding en 2.000.000 bytes totaal voor postafbeeldingen vermeldt. Reken met het kleinere getal.

Let op: cijfers hier zijn geverifieerd tegen de Bluesky-documentatie over rate limits per september 2026. Platforms wijzigen dit zonder aankondiging.

Wat kost je daadwerkelijk drie weken?

Bluesky is echt het goedkoopst te integreren van de grote platforms, maar “goedkoop” is niet “gratis”.

Facetberekening voor echte tekst. URL’s, afsluitende leestekens, handles en hashtags detecteren, elke match omzetten naar UTF-8-byte-offsets, en dat correct houden als een gebruiker de tekst bewerkt. Hier zitten de bugs.

Blobbudgettering. 2 MB totaal over maximaal 4 afbeeldingen betekent server-side resizen en opnieuw coderen, EXIF strippen, en beslissen wat te doen als de foto’s van de gebruiker niet passen.

Linkkaarten. Om posts eruit te laten zien als posts haal je de doelpagina op, trek je titel, beschrijving en afbeelding eruit, upload je die afbeelding als blob, en bouw je de external-embed. Dat is een kleine crawler met timeouts en foutafhandeling.

Sessiecaching, vanwege het plafond van 30 per 5 minuten, en PDS-hostresolutie, want ervan uitgaan dat het bsky.social is, breekt zelfgehoste accounts op een manier die je van jouw kant niet kunt oplossen.

Als je content spiegelt, cross-posten van X naar Bluesky en Threads cross-posten naar Bluesky behandelen beide de verschillen in tekstlengte en media die je moet oplossen voordat het record al geldig is.

De korte versie

  • Drie aanroepen: createSession, uploadBlob voor afbeeldingen, createRecord met een app.bsky.feed.post record.
  • Geen app-review, geen OAuth-scherm. App-wachtwoorden, overhandigd door de gebruiker.
  • Links en vermeldingen hebben facetten met byte-offsets over UTF-8 nodig. Niets wordt automatisch gedetecteerd.
  • 4 afbeeldingen per post, elk 1.000.000 bytes, 2.000.000 bytes totaal, alt-tekst verplicht.
  • Schrijfacties kosten 3 punten per create tegen 5.000/uur en 35.000/dag. createSession is 30 per 5 minuten.
  • Accounts kunnen op een zelfgehoste PDS wonen. Hardcodeer de host niet.

Posten naar Bluesky naast 14 andere netwerken

Bluesky is de makkelijkste. Hetzelfde product heeft meestal ook X nodig (OAuth 2.0 PKCE, gefragmenteerde media-upload, facturatie per verzoek), Threads (Meta App Review, container-dan-publiceren, tokenverversing na 60 dagen) en TikTok (een Content Posting API-audit voordat je überhaupt kunt publiceren). Elk heeft zijn eigen authenticatie, mediapijplijn en asynchroon faalmodel.

BulkPublish is één REST API voor 15 platforms, Bluesky inbegrepen, met facetberekening, blobresizing en sessievernieuwing server-side afgehandeld. De developer docs en de REST API-referentie hebben de endpoints, en Bluesky posts inplannen behandelt dit zonder code.

Gerelateerd