Publicar en Bluesky requiere tres llamadas: com.atproto.server.createSession para obtener los tokens, com.atproto.repo.uploadBlob si tienes imágenes, y com.atproto.repo.createRecord para escribir un registro app.bsky.feed.post. No hay revisión de aplicación ni pantalla de consentimiento de OAuth que aprobar. El costo de esa simplicidad es que los enlaces y las menciones no se detectan por ti: calculas tú mismo los desplazamientos en bytes sobre UTF-8.
¿Qué puede publicar la API de Bluesky, y qué no?
Un registro de publicación es un documento JSON simple. Los campos obligatorios son $type (app.bsky.feed.post), text y createdAt como marca de tiempo ISO 8601. Todo lo demás es estructura opcional.
| Función | Compatibilidad |
|---|---|
| Imágenes por publicación | 4 como máximo |
| Tamaño de imagen | 1.000.000 bytes cada una, según la documentación de publicaciones |
| Tamaño total de blobs por publicación | 2.000.000 bytes como máximo |
| Texto alternativo | Obligatorio por imagen, con una relación de aspecto |
| Publicaciones cita | app.bsky.embed.record |
| Tarjetas de enlace | app.bsky.embed.external, con tu propia miniatura |
| Respuestas | reply con referencias sólidas root y parent |
| Etiquetas de idioma | langs, un array como ["en-US"] |
Las carencias que conviene mencionar de entrada:
- Nada se detecta automáticamente. Los enlaces, las menciones y los hashtags son texto inerte a menos que adjuntes facets. Una URL que pegas sin un facet no es clicable.
- Las tarjetas de enlace son tu trabajo.
app.bsky.embed.externalrequiere título, descripción y miniatura. Nada extrae esos datos de la página por ti. - Las menciones necesitan un DID resuelto. No puedes poner un identificador en un facet; primero resuelves el identificador a un DID.
- Las imágenes deben tener los metadatos EXIF eliminados antes de subirlas, según la documentación.
- La documentación de publicaciones indica que no hay límite de caracteres o grafemas. No pudimos verificar una cifra en esa página, así que no citamos ningún número. Consulta la publicación sobre el límite de caracteres de Bluesky para ver lo que impone el cliente.
Nota: las cifras aquí se verificaron contra la documentación para desarrolladores de Bluesky (docs.bsky.app, que ahora redirige a bsky.network/docs) a septiembre de 2026. Las plataformas cambian esto sin previo aviso.
¿Cuál es el modelo de autenticación?
Esta es la sección de autenticación más corta que leerás sobre cualquier plataforma social.
No hay registro de aplicación, ni revisión de aplicación, ni pantalla de consentimiento de OAuth para la ruta de contraseña de aplicación. Un usuario crea una contraseña de aplicación en su configuración de Bluesky y se la entrega a tu software. Intercambias el identificador más esa contraseña de aplicación por un JWT de acceso y un JWT de actualización en com.atproto.server.createSession. Los tokens de acceso son de corta duración; los renuevas con el JWT de actualización.
Dos consecuencias. Una contraseña de aplicación es una credencial que tu usuario te entrega directamente: sin pantalla de consentimiento no hay concesión de alcance mediada por la plataforma, así que la obligación de almacenamiento recae por completo en ti. Cífralas, y dale a los usuarios una forma visible de desconectarse.
Y la red no pertenece a una sola empresa. Las cuentas de AT Protocol viven en un servidor de datos personal (PDS), y un PDS se puede autoalojar. Tu cliente habla con el host del PDS del usuario, así que fijar bsky.social funciona hoy pero no es el modelo del protocolo. Lee el host desde el documento DID del usuario.
¿Cómo se publica realmente? La secuencia de llamadas
POST /xrpc/com.atproto.server.createSessionconidentifier(identificador o DID) ypassword(la contraseña de aplicación). DevuelveaccessJwt,refreshJwtydid.- Si tienes imágenes:
POST /xrpc/com.atproto.repo.uploadBlobpor cada imagen, con los bytes en bruto y elContent-Typecorrecto. Cada llamada devuelve una referencia de blob. - Calcula los facets para cualquier enlace o mención, como desplazamientos en bytes dentro de la codificación UTF-8 de
text. POST /xrpc/com.atproto.repo.createRecordconrepoconfigurado como tu DID,collectionconfigurado comoapp.bsky.feed.post, y el registro en sí.
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 }],
},
],
},
}),
});
La aritmética de los facets, explicada
byteStart y byteEnd son desplazamientos dentro de los bytes UTF-8 del texto, no dentro de los índices de cadena de JavaScript ni de los caracteres.
"café".length es 4 en JavaScript, pero la codificación UTF-8 tiene 5 bytes. Cualquier emoji antes de tu enlace desplaza el offset en 4 bytes mientras mueve el índice de la cadena en 2. Si te equivocas aquí, el enlace se renderiza como texto roto o resalta el tramo equivocado, sin ningún error del servidor: el registro es válido, solo que apunta a los bytes incorrectos.
Codifica la cadena una sola vez, encuentra los desplazamientos en el array de bytes, y nunca mezcles los dos sistemas de coordenadas. La función de facet de enlace usa uri, no url.
¿Cuáles son los límites de velocidad de Bluesky?
Las escrituras se miden con un sistema de puntos por cuenta: CREATE cuesta 3 puntos, UPDATE 2, DELETE 1, contra un presupuesto de 5.000 puntos por hora y 35.000 por día. Eso equivale aproximadamente a 1.666 creaciones por hora y 11.666 por día.
| Límite | Valor |
|---|---|
| Puntos de escritura | 5.000/hora, 35.000/día |
| Creaciones (derivadas) | ~1.666/hora, ~11.666/día |
| Solicitudes totales al PDS | 3.000 cada 5 minutos, por IP |
createSession | 30 cada 5 minutos, 300 al día, por cuenta |
| Límite de subida de blobs | 52.428.800 bytes (50 MB) |
El límite de createSession es el que atrapa a los programadores. 30 cada 5 minutos por cuenta significa que hay que guardar la sesión en caché y renovarla, en lugar de iniciar sesión en cada publicación. Un proceso que crea una sesión en cada tarea se autolimitará mucho antes de llegar a cualquier límite de publicación.
Fíjate en las dos cifras de blobs: el PDS acepta blobs de hasta 50 MB, mientras que la documentación de publicaciones indica un límite de 1.000.000 bytes por imagen y 2.000.000 bytes en total para las imágenes de una publicación. Ajústate al más pequeño.
Nota: las cifras aquí se verificaron contra la documentación de límites de velocidad para desarrolladores de Bluesky a septiembre de 2026. Las plataformas cambian esto sin previo aviso.
¿Qué es lo que realmente te va a costar tres semanas?
Bluesky es realmente la más barata de integrar entre las grandes plataformas, pero “barata” no es “gratis”.
El cálculo de facets para texto real. Detectar URLs, puntuación al final, identificadores y hashtags, convertir cada coincidencia a desplazamientos en bytes UTF-8, y mantener eso correcto cuando un usuario edita el texto. Ahí es donde viven los errores.
La gestión del presupuesto de blobs. Un total de 2 MB entre hasta 4 imágenes significa redimensionar y recodificar en el servidor, eliminar los metadatos EXIF y decidir qué hacer cuando las fotos del usuario no caben.
Las tarjetas de enlace. Para que las publicaciones parezcan publicaciones reales, tienes que obtener la página de destino, extraer título, descripción e imagen, subir esa imagen como blob y construir el embed external. Eso es un pequeño rastreador con tiempos de espera y gestión de fallos.
El almacenamiento en caché de sesiones, por el límite de 30 cada 5 minutos, y la resolución del host del PDS, porque asumir bsky.social rompe las cuentas autoalojadas de una forma que no puedes solucionar desde tu lado.
Si estás reflejando contenido, cross-posting de X a Bluesky y cross-posting de Threads a Bluesky cubren ambos las diferencias de longitud de texto y de multimedia que hay que resolver antes de que el registro sea siquiera válido.
La versión corta
- Tres llamadas:
createSession,uploadBlobpara imágenes,createRecordcon un registroapp.bsky.feed.post. - Sin revisión de aplicación, sin pantalla de OAuth. Contraseñas de aplicación, entregadas por el usuario.
- Los enlaces y las menciones necesitan facets con desplazamientos en bytes sobre UTF-8. Nada se detecta automáticamente.
- 4 imágenes por publicación, 1.000.000 bytes cada una, 2.000.000 bytes en total, texto alternativo obligatorio.
- Las escrituras cuestan 3 puntos por creación contra 5.000/hora y 35.000/día.
createSessiones 30 cada 5 minutos. - Las cuentas pueden vivir en un PDS autoalojado. No fijes el host de forma rígida.
Publicar en Bluesky junto con otras 14 redes
Bluesky es la fácil. El mismo producto normalmente también necesita X (OAuth 2.0 PKCE, subida de multimedia por partes, facturación por solicitud), Threads (revisión de aplicaciones de Meta, contenedor y luego publicación, renovación de token cada 60 días) y TikTok (una auditoría de la API de publicación de contenido antes de poder publicar siquiera). Cada una tiene su propia autenticación, su propio flujo de multimedia y su propio modelo de fallos asíncrono.
BulkPublish es una sola API REST en 15 plataformas, Bluesky incluida, con el cálculo de facets, el redimensionado de blobs y la renovación de sesión gestionados en el servidor. La documentación para desarrolladores y la referencia de la API REST tienen los endpoints, y cómo programar publicaciones en Bluesky lo cubre sin código.