API abierta de comunidades
Una API de ámbito comunidad: una clave queda ligada a una comunidad y actúa en nombre de los miembros que indiques. No es la API del panel de creador ni la API de miembros: aquí no hay sesión de inicio de sesión.
Antes de empezar
Dos cosas tienen que cumplirse antes de que ninguna clave funcione, y ninguna de las dos se arregla desde tu código.
1. El plan de la comunidad tiene que incluir acceso a la API
Esto lo controla quien sea propietario de la comunidad, no quien integra. El plan necesita la función de acceso a la API — si no, cada llamada devuelve 22203 — y una cuota diaria distinta de cero; de lo contrario cada llamada devuelve 22204.
Las comunidades creadas hace tiempo tienen el acceso a la API desactivado por defecto, incluso cuando el plan ya incluye otras funciones avanzadas. Si una clave recién emitida devuelve 22203 en cada llamada, pide a quien sea propietario que lo revise en la consola en lugar de releer tu código.
No hace falta descubrirlo acumulando errores: /capabilities/get lo responde directamente, y sigue funcionando en esos dos estados.
2. Crea una clave en la consola de administración de la comunidad
Administración de la comunidad → Integraciones → API Keys → Crear. Una clave es mfk_ seguido de 64 caracteres hexadecimales, y el valor completo se muestra exactamente una vez: después solo verás el prefijo.
Los ámbitos que marques al crearla deciden a qué endpoints puede llamar. No se pueden editar después; para cambiarlos, crea una clave nueva y revoca la anterior.
Opcionalmente puedes restringir una clave a una lista de IP permitidas (direcciones sueltas o rangos CIDR). Las peticiones desde cualquier otro sitio fallan con 10007, el mismo código que una clave inválida, algo que conviene recordar cuando una clave que ayer funcionaba deja de hacerlo desde otro host.
Ámbitos
| Ámbito | Cubre |
|---|---|
| site.read | Perfil y ajustes de la comunidad |
| content.read | Lectura de publicaciones, feeds, búsqueda y árboles de respuestas |
| content.write | Crear, editar y borrar publicaciones |
| media.read | URL firmadas de los medios almacenados |
| media.write | Tokens de subida de medios; por compatibilidad, también lee URL firmadas |
| spaces.read | Directorio y detalle de espacios |
| members.read | Lectura de miembros y búsqueda por correo |
Lo que significa * cambió en septiembre de 2026
Antes quería decir «todo, incluidos los ámbitos que se añadan después», lo que ampliaba en silencio cada clave emitida cada vez que la plataforma lanzaba una capacidad nueva. Ahora una clave nueva guarda la lista concreta vigente en el momento de crearse, y un * heredado cubre únicamente los cuatro ámbitos que existían cuando se firmó: site.read, content.read, content.write y media.write.
Por eso una clave antigua no puede llamar a los endpoints nuevos: espacios, miembros y lectura de URL firmadas. Emite una clave nueva con esos ámbitos marcados. Es deliberado: nadie debería entregar su directorio de miembros porque la plataforma haya lanzado una función nueva.
Autenticación
Envía tu clave en una de las dos cabeceras. Son equivalentes; si mandas ambas, gana X-API-Key. La comunidad se deduce de la clave, así que nunca pasas un id de sitio.
X-API-Key: mfk_xxx
Authorization: Bearer mfk_xxx
La clave es la credencial en sí misma: guárdala en el servidor. No debe estar ni en código de navegador ni en un paquete móvil, donde cualquiera puede leerla y publicar como tus miembros.
Convenciones
Se cumplen en todos los endpoints de abajo.
- Todos los endpoints son POST. No hay GET, PUT ni DELETE: las lecturas también son POST.
- Envía Content-Type: application/json. Los endpoints que no reciben parámetros siguen esperando un cuerpo JSON vacío.
- Todos los id son cadenas hashid como "kZ3mQ9x": comunidad, usuario, publicación, espacio y medio por igual. Los id numéricos se rechazan.
- Las marcas de tiempo se devuelven en RFC3339, por ejemplo 2026-08-18T10:00:00Z.
- Los importes son enteros en la unidad monetaria más pequeña (céntimos).
- Una cabecera opcional Accept-Language (zh, en, ja, ko, es, fr, de, pt) traduce los mensajes de error.
- La comunidad se deduce de tu clave de API. No envíes un id de sitio ni en la ruta ni en el cuerpo.
Hay una entrada que rompe esa regla: scheduled_at en /post/create y /post/update es una marca de tiempo Unix i64 en segundos, no una cadena. Mira más abajo la sección de publicaciones programadas.
El éxito y el fallo comparten un mismo sobre
{
"code": 0,
"msg": "success",
"data": { }
}Endpoints
Todos los endpoints viven bajo /site_open_api/v1, usan POST con cuerpo JSON y requieren en tu clave el ámbito indicado.
Los ámbitos de tu clave, si el acceso a la API está activo, y la cuota usada, la restante y su hora de reinicio. Es el único endpoint que sigue respondiendo cuando el plan o la cuota diaria bloquean todo lo demás, y no consume cuota. Un quota_limit de -1 significa ilimitado; los tres campos de cuota aparecen y desaparecen juntos, así que comprueba que existen antes de restar.
Perfil, ajustes y metadatos de la comunidad
Una publicación por id
Obligatorio: id
Hasta 50 publicaciones a la vez; los id que no aparezcan vuelven en missing_ids en lugar de hacer fallar el lote
Obligatorio: ids
Buscar publicaciones: el campo es query, no keyword
Obligatorio: query
Publicaciones destacadas, opcionalmente acotadas a un espacio
Todas las respuestas de una publicación
Obligatorio: post_id
Solo las respuestas de primer nivel
Obligatorio: post_id
El subárbol que cuelga de una respuesta
Obligatorio: post_id
La cadena de ancestros de una respuesta
Obligatorio: post_id
Feed de la comunidad, lo más reciente primero (cursor de texto). since_id solo trae publicaciones más nuevas: no ve ediciones ni borrados, así que sirve para completar una línea temporal, no para sincronizarla
Feed de la comunidad por puntuación (cursor de coma flotante)
Feed de destacados (cursor de texto)
El feed de un espacio, con orden, búsqueda y filtros de preguntas y respuestas opcionales (cursor de texto)
Obligatorio: space_id
El feed de un espacio por puntuación (cursor de coma flotante): la entrada antigua, conservada por compatibilidad
Obligatorio: space_id
Publicar como un miembro. El cuerpo, el espacio, el título, la respuesta o cita de destino, los medios, las encuestas, el audio y la programación son todos opcionales
Obligatorio: author_user_id, idempotency_key
Editar una publicación; pasa version para bloqueo optimista
Obligatorio: actor_user_id, idempotency_key, post_id
Borrar una publicación
Obligatorio: actor_user_id, idempotency_key, post_id
Los espacios visibles para el observador: omite viewer_user_id para la vista anónima, solo con lo públicamente legible
Un espacio por id
Obligatorio: id
Un miembro por id
Obligatorio: id
Resolver una dirección de correo exacta a un miembro
Obligatorio: email
Token de corta vida para el servicio de subida de medios
Obligatorio: author_user_id
URL firmadas para medios privados (caducan: vuelve a firmarlas, no las guardes)
Obligatorio: author_user_id, items, access_level
curl -X POST https://api.mateflow.com/site_open_api/v1/capabilities/get \
-H "X-API-Key: mfk_xxx" -H "Content-Type: application/json" -d '{}'
# → data: {
# "api_version": "v1",
# "site_id": "kZ3mQ9x",
# "scopes": ["site.read", "content.read"],
# "api_access": true,
# "quota_limit": 5000, // -1 means unlimited
# "quota_used": 128,
# "quota_reset_at": "2026-09-15T00:00:00Z"
# }- Las lecturas que aceptan una lista de id se topan en 50, y el limit de todos los endpoints paginados también se topa en 50.
- En /space/feed/list la codificación del cursor sigue a sort, así que cambiar de orden implica reiniciar la paginación. Los espacios de preguntas y respuestas ignoran sort y ordenan por qa_sort. Las integraciones nuevas deberían usar /space/feed/list con sort=top en lugar de /space/feed/top.
- Las respuestas de miembro son una proyección estrecha: id, username, display_name, avatar_url, status, role y created_at. El correo nunca se devuelve, ni siquiera cuando has buscado por él: esa dirección ya la tienes, y devolver la de cada miembro convertiría /member/get en una exportación de contactos. La búsqueda es solo por coincidencia exacta, y no encontrar nada es un éxito normal, no un error, así que los aciertos y los fallos no se distinguen ni por código de estado ni por tiempo de respuesta. También tiene el límite de tasa más estricto de la API.
Actuar en nombre de un miembro
La API no tiene sesión de inicio de sesión, así que quién actúa —y para quién se representan los resultados— siempre va explícito en el cuerpo de la petición. Los tres reciben un hashid de miembro.
author_user_idEl miembro con el que se publica la nueva publicación. Debe pertenecer ya a esta comunidad; si no, 20303.
actor_user_idQuién ejecuta una edición o un borrado. El permiso se evalúa contra este miembro: quien no sea autor ni administrador recibe 10004.
viewer_user_idOpcional en las lecturas. Devuelve los resultados tal como los ve ese miembro: espacios privados, estado de «me gusta» y de guardado. Omítelo para la vista anónima, solo pública.
Paginación
- Los feeds y las listas devuelven sus elementos junto a next_cursor. Devuelve next_cursor tal cual para pedir la página siguiente; un next_cursor vacío significa que has llegado al final.
- limit se topa en 50 en todas partes.
- En /space/feed/list el cursor codifica el orden actual. Cambiar sort a mitad del listado lo invalida: vuelve a la primera página.
Cuidado: los cursores de /feed/top y /space/feed/top son puntuaciones en coma flotante, no cadenas. Guardar ambos tipos en una misma variable de texto rompe la paginación en silencio.
Idempotencia
Toda escritura lleva un idempotency_key en el cuerpo de la petición. Aquí hay exactamente una capa de idempotencia; si ya conoces la de la plataforma basada en cabecera, lee la nota de abajo.
- idempotency_key es obligatorio en cada escritura y se topa en 190 caracteres. Reutilizar uno nunca crea una segunda publicación.
- Su ámbito es la comunidad, el tipo de petición y la clave juntos, con independencia de qué clave de API uses: si dos claves distintas de la misma comunidad envían el mismo idempotency_key al mismo endpoint, la segunda llamada repite el resultado de la primera.
- La misma clave con un cuerpo distinto devuelve un error de conflicto estable en lugar de colar en silencio como un reintento correcto.
- Si una petición expira, reintenta con la misma clave; nunca generes una nueva.
La cabecera general Idempotency-Key de la plataforma —la de la caché de respuesta de 24 horas— no se aplica a /site_open_api/**. Esa capa está por delante de la autenticación por clave, de modo que un acierto de caché se saltaría por completo la verificación de la clave. No envíes la cabecera esperando que haga algo aquí.
Publicaciones programadas
La programación es el único punto donde tanto el formato de entrada como el comportamiento se apartan del resto.
- scheduled_at es una marca de tiempo Unix i64 en segundos, no la cadena RFC3339 que usan todas las demás fechas.
- En /post/update, omitir scheduled_at deja la programación existente intacta. No es un borrado.
Pasar un valor de 0 o menor cancela la programación y devuelve la publicación a borrador, pero solo las publicaciones de blog tienen estado de borrador. En una publicación normal, esa misma llamada devuelve 10005 con schedule_cancel_unsupported, porque por debajo la ruta es «publicar ahora»: aceptarla sacaría el contenido antes de tiempo, dispararía el feed y las notificaciones, y no dejaría nada que deshacer.
Subir medios
Los archivos van al servicio de medios, no al host de la API. Tres pasos:
- Cambia tu clave de API por un token de subida de corta vida en /media/get_token.
- Envía el archivo por POST como multipart al servicio de medios, con el token en una cabecera llamada literalmente "token" y file_cate con uno de estos valores: Media, Avatar, Header, Audio o File.
- Pasa el id de medio devuelto en media_ids al crear la publicación.
Cambio incompatible, septiembre de 2026
Un token emitido sin ámbito explícito ahora solo tiene permiso de subida; antes llevaba subida, lectura y borrado juntos. Si usabas ese token para leer o borrar, pasa explícitamente un ámbito read o delete. Por otra parte, a una comunidad en periodo de gracia —prueba caducada, sin tarjeta registrada— el plan le deniega el token de subida; los de lectura y borrado no se ven afectados.
# 1. Exchange the API key for a short-lived upload token.
# Since 2026-09 a token with no explicit scope is upload-only.
curl -X POST https://api.mateflow.com/site_open_api/v1/media/get_token \
-H "X-API-Key: mfk_xxx" -H "Content-Type: application/json" \
-d '{"author_user_id": "kZ3mQ9x", "scope": "upload"}'
# 2. Upload the file to the media service (token goes in a "token" header)
curl -X POST https://media.mateflow.com/api/v1/media/upload \
-H "token: <token from step 1>" \
-F "file_cate=Media" -F "file=@photo.jpg"
# 3. Attach the media id when creating the post
curl -X POST https://api.mateflow.com/site_open_api/v1/post/create \
-H "X-API-Key: mfk_xxx" -H "Content-Type: application/json" \
-d '{"author_user_id":"kZ3mQ9x","idempotency_key":"6f1c...","body":"Hi","media_ids":["m8Yq2Lp"]}'Los medios privados se leen con /media/get_signed_urls, que recibe los items y un access_level de 1 (público), 2 (semiprivado) o 3 (privado) y devuelve URL con caducidad. Pídelas cuando las necesites en lugar de guardarlas, o empezarán a devolver 403.
Adjuntar medios a una publicación
- Envía o bien medias —una lista de pares media_id y alt— o bien media_ids, una lista simple de id. Enviar ambos devuelve 10005.
- alt es opcional. Una cadena vacía borra el texto alternativo existente; omitir el campo lo deja como estaba.
Códigos de error
El éxito y el fallo comparten el sobre, y el estado HTTP sigue al código de negocio. Ramifica por code, no por el estado.
| Código | HTTP | Significado |
|---|---|---|
| 0 | 200 | Correcto |
| 10005 | 400 | Parámetros inválidos: msg indica el campo que falla |
| 10007 | 401 | Clave inválida, revocada o caducada, o la IP de origen no está en la lista permitida |
| 21304 | 403 | La clave no tiene el ámbito que exige este endpoint; los ámbitos se fijan al crearla |
| 22203 | 403 | El plan no incluye acceso a la API; consulta data.required_plan |
| 22204 | 403 | Cuota diaria agotada; consulta data.current y data.limit |
| 20303 | 404 | author_user_id / viewer_user_id no es miembro de esta comunidad |
| 10003 | 404 | El registro de destino no existe |
| 10004 | 403 | El actor no tiene permiso sobre este recurso |
| 10202 | 429 | Límite de tasa alcanzado: reduce el ritmo y respeta la cabecera Retry-After |
| 10001 | 500 | Error del servidor: se puede reintentar sin riesgo |
Ojo a la trampa: 22203 y 22204 son 403, no 429. Solo 10202 es limitación de tasa. Detéctalo por code, nunca por el estado HTTP: es la lectura errónea más frecuente de esta API.
Las respuestas de error llevan un request_id. Cítalo al reportar un problema: es lo que nos permite encontrar exactamente tu llamada en los registros.
Los errores de plan llevan metadatos estructurados
Ambos errores de plan vuelven con detalle suficiente para decirle a la persona propietaria qué hacer, de modo que puedas mostrar un mensaje útil en lugar de «algo ha salido mal».
// The plan does not include API access
{
"code": 22203,
"msg": "...",
"request_id": "00de5640-8ae5-4a71-aef8-2f2cab0ce8a7",
"data": {
"error_code": "feature_not_available",
"feature": "api_access",
"required_plan": "Growth"
}
}
// The community is out of daily quota
{
"code": 22204,
"data": {
"error_code": "quota_exceeded",
"resource": "api_requests_per_day",
"current": "5001",
"limit": "5000",
"required_plan": "Business"
}
}Cuota y límites de tasa
Tres mecanismos distintos. Confundirlos es la razón de que se reporte que una integración «está limitada» cuando en realidad se ha quedado sin cuota del plan.
| Capa | Qué cuenta | Al superarlo |
|---|---|---|
| Cuota diaria del plan | Un día entero, todos los endpoints, por comunidad | 22204 / HTTP 403 |
| Presupuesto de ráfaga por clave | Ventanas de 10 y 60 segundos, por clave, escalonado por endpoint | 10202 / HTTP 429 |
| Límite general por IP | Por IP de origen, 60 cada 10 s y 300 por minuto | 10202 / HTTP 429 |
La cuota diaria cuenta las peticiones que superan la autenticación, se reinicia a las 00:00 UTC e informa en data.limit del techo realmente vigente, incluida cualquier ampliación concedida a esa comunidad en concreto. Si el backend del contador falla, la petición pasa en lugar de rechazarse, así que un recuento bajo ocasional no significa que la cuota haya dejado de funcionar.
Presupuesto de ráfaga por clave (nuevo en septiembre de 2026)
Antes el único cubo era por IP, de modo que varios clientes tras una misma dirección de salida competían entre sí mientras que quien repartía sus llamadas entre muchas direcciones apenas tenía restricciones. Ahora el presupuesto se escalona por clave: lo que quien es propietario emite, puede rotar y de lo que responde.
| Nivel | Cubre | Por 10 s | Por 60 s |
|---|---|---|---|
| Lecturas | Feeds, publicaciones, respuestas, espacios, introspección | 120 | 600 |
| Medios | get_token y get_signed_urls | 30 | 120 |
| Escrituras | Crear, actualizar y borrar publicaciones | 20 | 120 |
| Búsqueda | post/search | 20 | 60 |
| Búsqueda de miembros | member/lookup | 5 | 10 |
member/lookup es mucho más estricto por seguridad, no por capacidad. Sus otras restricciones limitan cuánto revela una sola respuesta; esta limita cuántas veces puedes preguntar, y ese es justo el paso que convierte una búsqueda en una recolección de directorio. Usado como está pensado —resolver al ritmo de una persona direcciones que ya tienes—, diez por minuto sobran.
- Superarlo devuelve HTTP 429 con code 10202, además de Retry-After y las cabeceras X-RateLimit-Limit, X-RateLimit-Remaining y X-RateLimit-Reset.
- Toma estas cifras como punto de partida, no como una medición: se ajustan según el tráfico observado. No las fijes en tu lógica de reintentos; respeta el 429 y Retry-After. Toda la capa de límite de tasa está desactivada por defecto y funciona un tiempo en modo observación antes de aplicarse.
- Implementa el retroceso ante 429 en cualquier caso, y mantén el tráfico de lectura en torno a diez peticiones por segundo o menos.
Recibir webhooks
Si quien es propietario de la comunidad ha configurado un endpoint de webhook o ha conectado Slack, Discord o Zapier, las entregas llevan dos juegos de id que significan cosas distintas.
| Campo | Identifica | Entre reintentos |
|---|---|---|
| event_id | El evento de negocio | Estable |
| occurred_at | Cuándo ocurrió el evento | Estable |
| delivery_id | Este intento de entrega | Cambia |
| timestamp | Cuándo se envió este intento | Cambia |
Deduplica por event_id. Una entrega que procesaste bien pero contestaste demasiado tarde vuelve a llegar con un delivery_id nuevo y el mismo event_id. Cuando un evento va a la vez a un webhook propio y a un Zap, ambos lados ven el mismo event_id, así que puedes cuadrarlos.
La entrega es al menos una vez y puede llegar desordenada. Tanto los duplicados como la llegada tardía de eventos antiguos son normales; quien recibe tiene que ser idempotente.
event_id puede faltar. Los trabajos encolados antes de que existiera el campo no lo llevan, y la entrega omite el campo por completo en lugar de enviar un 0: un 0 haría que todos los trabajos antiguos parecieran el mismo evento. Acepta que falte y recurre a una deduplicación de mejor esfuerzo con delivery_id.
Resolución de problemas
Los síntomas que llegan de verdad, y en qué suelen quedar.
| Síntoma | Normalmente |
|---|---|
| 10007 invalid API key, pero la clave se acaba de copiar de la consola | Espacios o un salto de línea alrededor de la clave; o la clave fue revocada; o hay una lista de IP permitidas y tu dirección de salida no está en ella |
| 22203 feature_not_available | El plan tiene el acceso a la API desactivado. Tras cambiarlo, deja pasar hasta 10 minutos a que caduque la caché del plan |
| 22204 quota_exceeded con un limit de 0 | La cuota diaria del plan es 0, lo que significa no disponible, no «sin configurar y por tanto ilimitada» |
| 21304 en un endpoint que esperabas poder usar | La clave no tiene ese ámbito. Los ámbitos se fijan al crearla, así que la solución es una clave nueva |
| 21304 en una clave con «todos los permisos» (*) | Un * heredado no cubre los ámbitos añadidos después: media.read, spaces.read, members.read. Emite una clave nueva con ellos marcados |
| 10202 con HTTP 429 | Un límite de tasa. Reduce el ritmo según Retry-After; si fue en member/lookup, recuerda que ese cubo son diez por minuto |
| 10005 con schedule_cancel_unsupported | Pasaste un scheduled_at de 0 o menor en una publicación normal. Solo las de blog tienen un estado de borrador al que volver |
| Un token de medios que servía para leer o borrar ha dejado de funcionar | Desde septiembre de 2026 un token sin ámbito es solo de subida. Pasa un ámbito read o delete |
| /member/lookup devuelve 200 con found false | Eso es un «no encontrado» normal, no un error. Aciertos y fallos son ambos 200: ramifica por found |
| 10005 invalid params mencionando query | El campo de búsqueda es query. keyword no es un campo válido |
| 20303 user not found | El id no es miembro de esta comunidad, o has enviado un id numérico en lugar de un hashid |
| La paginación deja de avanzar | Los cursores de /feed/top y /space/feed/top son números en coma flotante. Serializados como texto dejan de coincidir |
| Las URL de medios empiezan a devolver 403 al cabo de un rato | Las URL firmadas tienen un TTL. Vuelve a firmarlas cuando haga falta en lugar de guardarlas |
Notas
- La comunidad se deduce de tu clave de API. No envíes un id de sitio ni en la ruta ni en el cuerpo.
- Los ámbitos se fijan al crear la clave. Para cambiarlos, crea una clave nueva y revoca la anterior.
- Guarda tus claves de API en el servidor. Una clave en código de cliente permite a cualquiera que la lea publicar como tus miembros.
- El contrato OpenAPI cubre cada ruta y cada esquema. Un matiz: el generador del que procede no emite required en los cuerpos de petición, así que toma los campos obligatorios de esta página y no del contrato.
Empieza a construir
Crea una clave con ámbitos en la consola de tu comunidad, llama a /capabilities/get para confirmarla y adelante.
Prueba gratis de 14 días · Sin tarjeta de crédito