Blastack

Desarrolladores

La API, y las tres herramientas con las que se conecta

Diecisiete extremos en diez direcciones, cinco sucesos salientes y una especificación OpenAPI 3.1 pública que Make y n8n importan solos. Aquí está entera: los parámetros uno a uno, ejemplos que funcionan tal cual y la lista de lo que no hace.

Pegad la primera dirección en el importador de n8n o de Make y los nodos salen solos. Zapier no lee OpenAPI: sus pasos están más abajo, escritos aparte por eso mismo.

De un vistazo

Lo que hace falta para la primera llamada

Ocho datos. Con estos ya se escribe la petición sin bajar más.

QuéValorNota
Raíz de la APIhttps://app.blastack.com/api/v1Vive en la aplicación y no en esta web. Una sola dirección, y no cambia.
Especificaciónhttps://app.blastack.com/api/openapi.jsonPública y sin clave, para que un importador la pueda leer antes de tener credenciales.
AutenticaciónAuthorization: Bearer blsk_<prefix>_<secret>En la cabecera, nunca en la dirección: las direcciones acaban en los registros del servidor y del proxy de delante.
Formatoapplication/json; charset=utf-8JSON entrando y saliendo. No hay XML ni formularios.
Límite120 req/min · RateLimit-RemainingPor clave y por minuto. Cada respuesta lleva cuántas quedan y cuándo se renueva la cuenta.
Paginaciónpage=1 · per_page=50 · ≤ 200La primera página es la uno, no la cero. Pedir más de doscientas se recorta en vez de fallar.
Errores{ error: { status, code, message } }Siempre el mismo objeto, con un código estable pensado para escribirle un condicional encima.
Entorno de pruebas—No hay. Se trabaja contra producción, y por eso conviene empezar con una clave de solo lectura.
curl https://app.blastack.com/api/v1/people?per_page=1 \
  -H "Authorization: Bearer blsk_a1b2c3d4e5f6_YOUR_SECRET"

Lo que hay

Tres superficies, y ninguna a medias

Están las tres funcionando. Lo que falta está más abajo, en su propio apartado y dicho por su nombre.

Aprovisionamiento SCIM 2.0

Que vuestro sistema de personal dé de alta, cambie de departamento y desactive a la gente sin que nadie lo copie a mano. Es la mitad que de verdad importa a informática: el día que despedís a alguien, la cuenta se desactiva sola.

Filtro por userName y externalId, paginación y modificación parcial. Se leen el departamento y la dirección de trabajo, así que quien entra por aquí nace con su departamento y su centro puestos. Dar de baja desactiva, no borra.

Inicio de sesión único (OpenID Connect)

Entrar con la cuenta de la empresa desde Entra ID, Google Workspace u Okta. Convive con el código de acceso, que es como entra la plantilla que no tiene correo corporativo.

Se configura pegando la dirección de descubrimiento del proveedor y se comprueba contra él antes de guardar. Los dominios de correo se verifican por DNS. No hay SAML ni LDAP.

API de Blastack

Publicar desde el programa que ya sabe que la línea 3 para el jueves, dar de baja a quien sale del sistema de personal, y que os llamemos nosotros cuando pasa algo en vez de tener que preguntar cada cinco minutos.

JSON sobre HTTPS, clave por cabecera, ámbitos por clave, errores con código estable y avisos salientes firmados. Es lo que se documenta en el resto de esta página.

Automatización

Zapier, Make y n8n, paso a paso

Las tres no se montan igual, y decirlo ahorra la tarde que se pierde descubriéndolo. Make y n8n leen la especificación y se configuran casi solos; Zapier no la lee, así que allí cada llamada se escribe a mano. Las tres funcionan; el trabajo no es el mismo.

Herramienta¿Lee la especificación?Disparador recomendado
n8nSí, enteraNodo «Webhook» + POST /webhooks
MakeSí, en el editor de aplicaciones propias«Custom webhook» + POST /webhooks
ZapierNo«Catch Hook» + POST /webhooks

n8n

El camino más corto de los tres. Se da de alta una credencial de cabecera, se pega la dirección de la especificación en el importador y queda un nodo por operación con sus parámetros ya puestos.

Pasos

  1. 01Credenciales → nueva → «Header Auth». En el nombre de la cabecera va Authorization y en el valor Bearer y vuestra clave, con un espacio entre medias.
  2. 02Nodo «HTTP Request», autenticación «Generic Credential Type» → «Header Auth», y la credencial de antes. Método y dirección salen de la tabla de extremos.
  3. 03Para no escribir los parámetros a mano, dadle al importador la dirección de la especificación: describe cada extremo con sus valores admitidos, así que lo que se genera trae ya los desplegables en vez de cajas de texto libre.
  4. 04Para disparar: nodo «Webhook», copiad su dirección de producción y registradla con POST /webhooks eligiendo los sucesos. Deja de hacer falta preguntar cada cinco minutos.

A tener en cuenta

El nodo de webhook responde 200 antes de ejecutar el resto del flujo, que es justo lo que hace falta: abandonamos a los diez segundos y eso cuenta como fallo.

Make

El módulo genérico de HTTP basta para todo, y quien vaya a montar muchos escenarios puede subir la especificación al editor de aplicaciones propias y quedarse con un módulo por operación.

Pasos

  1. 01Módulo «HTTP» → «Make an API Key Auth request». En la conexión, la clave se llama Authorization, el valor es Bearer y vuestra clave, y va en la cabecera.
  2. 02Método, dirección y cuerpo salen de la tabla de extremos y de los ejemplos de más abajo. El tipo de contenido es application/json.
  3. 03Si vais a montar varios escenarios, el editor de aplicaciones propias acepta la especificación y genera un módulo por operación: menos escenarios escritos a mano y menos erratas en los nombres de los filtros.
  4. 04Para disparar: módulo «Webhooks» → «Custom webhook», copiad su dirección y registradla con POST /webhooks. Make responde en el acto y procesa después, que es lo correcto aquí.

A tener en cuenta

Make cobra por operación: un escenario que pregunte cada cinco minutos gasta ocho mil operaciones al mes para no encontrar casi nada. Con el webhook se gasta una por suceso.

Zapier

Zapier no importa OpenAPI. Se monta con los pasos genéricos de webhooks, o construyendo una integración privada en su plataforma de desarrollo, donde cada extremo se declara a mano.

Pasos

  1. 01Disparador: «Webhooks by Zapier» → «Catch Hook». Copiad la dirección que os da y registradla con POST /webhooks eligiendo los sucesos.
  2. 02Acción: «Webhooks by Zapier» → «Custom Request». Método y dirección de la tabla, cabeceras Authorization con Bearer y vuestra clave y Content-Type con application/json, y el cuerpo en el campo de datos.
  3. 03Añadid un paso «Code by Zapier» que compruebe la firma antes de hacer nada. Zapier no la comprueba por vosotros y la dirección del disparador es pública: sin ese paso, cualquiera que la descubra puede inventarse una parada de línea.
  4. 04Si preferís nodos con nombre en vez de peticiones genéricas, hay que construir una integración privada y declarar allí cada extremo con sus campos. Es más trabajo y no lo ahorra la especificación.

A tener en cuenta

Sus disparadores por sondeo gastan una tarea en cada pasada, encuentren algo o no. El «Catch Hook» solo gasta cuando de verdad ha pasado algo.

Claves

Una clave por integración, con lo justo

Hoy las emitimos nosotros, a mano, y os decimos con qué ámbitos. No hay pantalla de autoservicio y está dicho abajo, entre lo que falta. Cada clave vive atada a una empresa y ninguna ruta lee de la petición a qué empresa preguntar, así que no hay dónde equivocarse.

  1. 01

    Pedidnos la clave

    Decidnos para qué es y qué tiene que poder hacer, y la emitimos con esos ámbitos y ninguno más. Se enseña una sola vez: de ella guardamos un resumen y nada más, así que si se pierde se revoca y se emite otra.

  2. 02

    Mandadla en cada petición

    En la cabecera de autorización, nunca en la dirección. Las direcciones acaban en el registro del servidor, en el del proxy de delante y en el historial de quien la pruebe a mano.

  3. 03

    Guardadla donde guardáis las demás

    En vuestro gestor de secretos, no en el repositorio. Empieza por un prefijo reconocible justamente para que los detectores de secretos la cacen si se escapa.

La API entra en el módulo de identidad corporativa, el mismo que abre el inicio de sesión único y el aprovisionamiento. Si la empresa no lo tiene contratado, la respuesta lo dice con esas palabras y no con un error genérico.

Los ámbitos, uno a uno

ÁmbitoQué abre
people:readLeer la plantilla y los departamentos y centros a los que se puede dirigir un aviso. Es lo único que necesita un cuadro de mando.
people:writeDar de alta, cambiar y desactivar personas. Desactivar cierra sus sesiones, así que no viene de regalo con el de leer.
announcements:readLeer lo publicado y quién ha confirmado una lectura obligatoria. Va aparte del de personas: una lista de nombres y puestos es el directorio, y el texto de los comunicados no.
announcements:writePublicar en el tablón. Respeta la cola de revisión de la empresa, así que una clave no salta el circuito de aprobación de nadie.
metrics:readLos indicadores agregados. Nadie sale con nombre y apellidos.
webhooks:readVer las direcciones registradas y el registro de entregas. Es lo que contesta «por qué no me llega nada» sin abrir un ticket.
webhooks:writeRegistrar, cambiar y borrar direcciones de aviso saliente. El más delicado de la lista: decide a qué servidor de internet mandamos el contenido de vuestros comunicados.

Extremos

Diecisiete operaciones en diez direcciones

Ordenadas por familia. La columna del ámbito es la que hay que mirar al pedir la clave: si falta, la respuesta dice cuál hacía falta y cuáles tiene la vuestra.

MétodoDirecciónÁmbitoQué hace
GET/peoplepeople:readLa plantilla, con su departamento, su centro y el identificador que tiene en vuestro sistema de personal. Diecisiete parámetros, entre ellos el filtro por fecha de última modificación, que es el que permite sincronizar cada cuarto de hora sin traérsela entera.
GET/people/{id}people:readUna persona concreta, buscada por nuestro identificador o por el vuestro. Sirve para refrescar una ficha sin recorrer la lista.
POST/peoplepeople:writeDa de alta a alguien. Es para cuando vuestro sistema de personal no habla SCIM; si lo habla, mejor SCIM, que además da de baja. El departamento y el centro se dan por nombre y se crean si no existen.
PATCH/people/{id}people:writeCambia solo los campos que mandéis. Uno que no vaya no se toca, así que una sincronización que solo conoce el puesto no puede borrarle el centro a nadie.
DELETE/people/{id}people:writeDesactiva; no borra nunca. Le cierra las sesiones en el acto, así que quien sale un viernes queda fuera ese viernes. Lo que publicó se queda.
GET/audiencespeople:readLos departamentos y los centros que existen, con cuánta gente hay en cada uno. Es de donde salen los nombres a los que se puede dirigir un aviso, y el filtro de grupos vacíos es el que encuentra un mapeo de aprovisionamiento roto.
GET/announcementsannouncements:readTodo lo publicado, con las lecturas y las confirmaciones de cada pieza. Incluye borradores y rechazados para que un cuadro de mando pueda pintar el embudo entero.
GET/announcements/{id}announcements:readUna publicación concreta. Es el paso siguiente al identificador que devuelve la publicación o que llega en un aviso saliente.
POST/announcementsannouncements:writePublica en el tablón. Hay que decir quién firma y no hay valor por omisión. Si esa persona no puede publicar sin revisión, entra en la cola y la respuesta lo dice.
GET/announcements/{id}/acknowledgementsannouncements:readQuién ha confirmado una lectura obligatoria y quién no, con nombre y hora. Es la única lista nominal de toda la API, y lo es porque ese es justamente el objeto de la lectura obligatoria: un acto voluntario, firmado y fechado que se le pueda enseñar a una inspección.
GET/metricsmetrics:readLa plantilla de hoy, las altas, las publicaciones y el alcance por publicación y por departamento. Los mismos números que la pantalla de analítica, calculados por el mismo código.
GET/webhookswebhooks:readVuestras direcciones registradas, cada una con su salud: fallos seguidos, qué contestó vuestro servidor la última vez y, si la hemos pausado, por qué.
POST/webhookswebhooks:writeRegistra una dirección y los sucesos que quiere. La respuesta trae el secreto de firma, y es la única vez que se ve.
GET/webhooks/{id}webhooks:readUna dirección concreta, con su salud.
PATCH/webhooks/{id}webhooks:writeCambia lo que mandéis. Poner la dirección otra vez activa levanta también la pausa que hayamos aplicado tras fallos repetidos y pone el contador a cero.
DELETE/webhooks/{id}webhooks:writeLa borra con su histórico. Para dejar de recibir un tiempo sin perder el histórico, es mejor desactivarla.
GET/webhooks/{id}/deliverieswebhooks:readTodo lo que hemos intentado mandaros: intentos, qué contestasteis y cuándo toca el siguiente. El identificador es el mismo que viajó en la cabecera, así que se puede cruzar con vuestros registros.

Parámetros

Todos, extremo a extremo

Ninguna lista está resumida: son las mismas que comprueba el servidor. Un parámetro que no esté aquí no existe, y escribirlo devuelve un error que dice cuáles admite ese extremo en vez de ignorarlo en silencio.

La columna de la derecha va en inglés a propósito. Sale del catálogo del que también se genera la especificación, así que es palabra por palabra lo que n8n y Make enseñan dentro de sus nodos. Traducirla aquí sería tener dos versiones que un día dirían cosas distintas.

GET /people

people:read

La plantilla, con su departamento, su centro y el identificador que tiene en vuestro sistema de personal. Diecisiete parámetros, entre ellos el filtro por fecha de última modificación, que es el que permite sincronizar cada cuarto de hora sin traérsela entera.

En la consulta

ParámetroTipoValores admitidosPor defectoEjemploQué hace
pageenteromínimo 112Page to return. The first page is 1, not 0.
per_pageenteromínimo 1 · máximo 200 · fuera de rango se recorta en vez de fallar50100How many items per page. Anything above 200 is clamped to 200 rather than rejected: asking for more is a reasonable thing to want.
sorttextocreated_at · updated_at · name · emailcreated_atField to sort by. Defaults to created_at.
ordertextoasc · descascSort direction.
fieldstexto · varios separados por comasexternal_id · email · name · job_title · department · center · role · status · language · created_at · updated_atexternal_id,department,statusComma-separated list of fields to return. `id` is always included. Use it to avoid pulling personal data you do not need.
statustexto · varios separados por comasactive · invited · disabledactiveOnly people in these states. Several may be given.
roletexto · varios separados por comasowner · admin · gestor_personas · responsable_local · informatico · editor · revisor · analista · employeeOnly people with these roles. Several may be given.
departmenttexto · varios separados por comasMaintenance,QualityDepartment name, or several separated by commas. Case-insensitive: your directory writing MANTENIMIENTO is not an error.
centertexto · varios separados por comasSite name, or several separated by commas. Case-insensitive.
no_departmentbooleanoOnly people with no department. These are the ones who never see a targeted announcement, so this is the filter that finds a broken provisioning mapping.
languagetexto · varios separados por comases · ca · en · pt · fr · it · de · sv · nl · pl · ro · csOnly people who picked this reading language.
emailtextoExact email address, case-insensitive.
external_idtextoExact external_id, as sent by your HR system over SCIM.
qtextogarciaFree text over name, email and job title. Case-insensitive, matches anywhere in the value.
updated_sincefecha ISO 86012026-08-01T00:00:00ZOnly people changed at or after this instant. This is the one that lets you sync every fifteen minutes without pulling two thousand rows every time.
created_sincefecha ISO 8601Only people added at or after this instant.
created_untilfecha ISO 8601Only people added before this instant.

GET /people/{id}

people:read

Una persona concreta, buscada por nuestro identificador o por el vuestro. Sirve para refrescar una ficha sin recorrer la lista.

En la dirección

ParámetroTipoValores admitidosPor defectoEjemploQué hace
idObligatoriotextoa3f1…Blastack id (a uuid) or the person's external_id from your HR system. A Blastack id is always a uuid and yours almost never is, so they cannot be confused; if one ever were, ours wins.

En la consulta

ParámetroTipoValores admitidosPor defectoEjemploQué hace
fieldstexto · varios separados por comasexternal_id · email · name · job_title · department · center · role · status · language · created_at · updated_atexternal_id,department,statusComma-separated list of fields to return. `id` is always included. Use it to avoid pulling personal data you do not need.

POST /people

people:write

Da de alta a alguien. Es para cuando vuestro sistema de personal no habla SCIM; si lo habla, mejor SCIM, que además da de baja. El departamento y el centro se dan por nombre y se crean si no existen.

En el cuerpo

ParámetroTipoValores admitidosPor defectoEjemploQué hace
nameObligatoriotextolargo máximo 120Ana GarcíaFull name, as it should appear on the wall.
emailtextolargo máximo 200Work email. Optional: most of the workforce this product is for has none, and they get in with an invite code instead. Unique within the company.
external_idtextolargo máximo 200EMP-0042The id this person has in your system. Store it and you can cross both lists later without matching names.
job_titletextolargo máximo 120Job title.
departmenttextolargo máximo 80Department name. Created if it does not exist. Without one, this person only ever sees company-wide announcements.
centertextolargo máximo 80Site name. Created if it does not exist.
roletextoadmin · gestor_personas · responsable_local · informatico · editor · revisor · analista · employeeemployeeWhat they can do. `owner` cannot be given from the API: a key pasted in a cron job must not be able to mint an account that can cancel the subscription.
statustextoactive · invited · disabledactive`active` can sign in; `invited` is created but not counted as signed up yet; `disabled` is created already deactivated, which is what a backfill of former staff needs.
languagetextoes · ca · en · pt · fr · it · de · sv · nl · pl · ro · csReading language. Leave it out and they read in the company's, which is what you want unless you actually know.

PATCH /people/{id}

people:write

Cambia solo los campos que mandéis. Uno que no vaya no se toca, así que una sincronización que solo conoce el puesto no puede borrarle el centro a nadie.

En la dirección

ParámetroTipoValores admitidosPor defectoEjemploQué hace
idObligatoriotextoa3f1…Blastack id (a uuid) or the person's external_id from your HR system. A Blastack id is always a uuid and yours almost never is, so they cannot be confused; if one ever were, ours wins.

En el cuerpo

ParámetroTipoValores admitidosPor defectoEjemploQué hace
nametextolargo máximo 120Full name.
emailtextolargo máximo 200Work email.
external_idtextolargo máximo 200The id this person has in your system.
job_titletextolargo máximo 120Job title.
departmenttextolargo máximo 80Department name. Created if it does not exist.
centertextolargo máximo 80Site name. Created if it does not exist.
roletextoadmin · gestor_personas · responsable_local · informatico · editor · revisor · analista · employeeWhat they can do. `owner` cannot be given from the API.
statustextoactive · invited · disabledTheir state. `disabled` also closes their sessions.
languagetextoes · ca · en · pt · fr · it · de · sv · nl · pl · ro · csReading language.

DELETE /people/{id}

people:write

Desactiva; no borra nunca. Le cierra las sesiones en el acto, así que quien sale un viernes queda fuera ese viernes. Lo que publicó se queda.

En la dirección

ParámetroTipoValores admitidosPor defectoEjemploQué hace
idObligatoriotextoa3f1…Blastack id (a uuid) or the person's external_id from your HR system. A Blastack id is always a uuid and yours almost never is, so they cannot be confused; if one ever were, ours wins.

GET /audiences

people:read

Los departamentos y los centros que existen, con cuánta gente hay en cada uno. Es de donde salen los nombres a los que se puede dirigir un aviso, y el filtro de grupos vacíos es el que encuentra un mapeo de aprovisionamiento roto.

En la consulta

ParámetroTipoValores admitidosPor defectoEjemploQué hace
typetextoall · departments · centersallReturn only departments, only sites, or both.
qtextoFree text over the name. Case-insensitive.
with_peoplebooleanoOnly groups that have at least one active person. A department with nobody in it is almost always an old name left behind, and it is the usual cause of an announcement that reaches no one.
sorttextoname · peoplenameField to sort by. Defaults to name.
ordertextoasc · descascSort direction.

GET /announcements

announcements:read

Todo lo publicado, con las lecturas y las confirmaciones de cada pieza. Incluye borradores y rechazados para que un cuadro de mando pueda pintar el embudo entero.

En la consulta

ParámetroTipoValores admitidosPor defectoEjemploQué hace
pageenteromínimo 112Page to return. The first page is 1, not 0.
per_pageenteromínimo 1 · máximo 200 · fuera de rango se recorta en vez de fallar50100How many items per page. Anything above 200 is clamped to 200 rather than rejected: asking for more is a reasonable thing to want.
sorttextopublished_at · created_at · updated_at · titlepublished_atField to sort by. Defaults to published_at.
ordertextoasc · descascSort direction.
fieldstexto · varios separados por comastitle · body · status · kind · language · audience · community · section · author · notify · comments_enabled · pinned · acknowledgements · reads · published_at · created_at · updated_at · urlexternal_id,department,statusComma-separated list of fields to return. `id` is always included. Use it to avoid pulling personal data you do not need.
statustexto · varios separados por comasdraft · pending_review · published · rejected · scheduled · archivedpublished,scheduledOnly these states. Several may be given. `scheduled` means it is written and waiting for its hour.
authortextoWho signs it: our id, their email or their external_id. Any of the three.
audience_typetextoall · departments · centersOnly announcements aimed this way.
departmenttexto · varios separados por comasOnly announcements aimed at these departments, by name. Several may be given.
centertexto · varios separados por comasOnly announcements aimed at these sites, by name.
communitytextoOnly announcements in this community, by id or by name.
requires_acknowledgementbooleanoOnly required reading, or only the rest. This is the filter a compliance report is built on.
pinnedbooleanoOnly pinned announcements, or only the rest.
qtextoFree text over title and body. Case-insensitive.
published_sincefecha ISO 8601Published at or after this instant.
published_untilfecha ISO 8601Published before this instant.
created_sincefecha ISO 8601Written at or after this instant.
updated_sincefecha ISO 8601Changed at or after this instant.

GET /announcements/{id}

announcements:read

Una publicación concreta. Es el paso siguiente al identificador que devuelve la publicación o que llega en un aviso saliente.

En la dirección

ParámetroTipoValores admitidosPor defectoEjemploQué hace
idObligatoriotextoThe announcement id.

En la consulta

ParámetroTipoValores admitidosPor defectoEjemploQué hace
fieldstexto · varios separados por comastitle · body · status · kind · language · audience · community · section · author · notify · comments_enabled · pinned · acknowledgements · reads · published_at · created_at · updated_at · urlexternal_id,department,statusComma-separated list of fields to return. `id` is always included. Use it to avoid pulling personal data you do not need.

POST /announcements

announcements:write

Publica en el tablón. Hay que decir quién firma y no hay valor por omisión. Si esa persona no puede publicar sin revisión, entra en la cola y la respuesta lo dice.

En el cuerpo

ParámetroTipoValores admitidosPor defectoEjemploQué hace
bodyObligatoriotextolargo máximo 8000The text. Plain text; line breaks are kept.
authorObligatoriotextoEMP-0042Who signs it: our id, their email or their external_id. Must be an active person in this company.
titletextolargo máximo 160Headline.
audienceobjetoWho it goes to. Leave it out and it goes to everyone. Names work as well as ids, case-insensitively, because your ERP has 'Maintenance' and not a uuid of ours. Nothing is created here: an unknown name is a typo in a script, and the reply tells you which one it was.
audience.typetextoall · departments · centersallAim at everyone, at departments or at sites.
audience.idslista["Maintenance","Quality"]Department or site names, or their ids. Required unless type is `all`.
communitytextoWhich community it belongs to, by id or by name. Leave it out and it goes on the main wall, which everybody sees. The author has to belong to the community, or be posting to the default one: an API key is not a way into a community its signer is not in.
sectiontextoWhich section it belongs to, by id or by name. It must belong to the same community.
publish_atfecha ISO 86012026-09-01T06:00:00ZPublish later, at this instant. It must be in the future. The reply comes back with `status: scheduled` and the wall shows it when the hour arrives. Leave it out to publish now. Note this means two clocks are involved — yours and ours — so if your system already schedules, prefer calling us at the right moment.
requires_acknowledgementbooleanofalseRequired reading: everyone it reaches gets a notification with a confirm button, and who confirmed is on record. Only signers who can publish without review may ask for it.
notifybooleanotrueWhether it makes a sound. Written the right way round on purpose: the screen has a 'publish without notifying' checkbox because a form that loses a field should still notify, but here you write the whole JSON and a field called `silent` reads wrong at three in the morning. Default is true.
comments_enabledbooleanotrueWhether people can comment on it.
pinnedbooleanofalseKeep it at the top of the wall.
languagetextoes · ca · en · pt · fr · it · de · sv · nl · pl · ro · csWhat language the text is written in. Leave it out and the company's is used. Getting this right matters: a Catalan original labelled as Spanish is shown to a Spanish reader without the 'not translated' notice that tells them nothing is broken.

GET /announcements/{id}/acknowledgements

announcements:read

Quién ha confirmado una lectura obligatoria y quién no, con nombre y hora. Es la única lista nominal de toda la API, y lo es porque ese es justamente el objeto de la lectura obligatoria: un acto voluntario, firmado y fechado que se le pueda enseñar a una inspección.

En la dirección

ParámetroTipoValores admitidosPor defectoEjemploQué hace
idObligatoriotextoThe announcement id.

En la consulta

ParámetroTipoValores admitidosPor defectoEjemploQué hace
pageenteromínimo 112Page to return. The first page is 1, not 0.
per_pageenteromínimo 1 · máximo 200 · fuera de rango se recorta en vez de fallar50100How many items per page. Anything above 200 is clamped to 200 rather than rejected: asking for more is a reasonable thing to want.
statetextoall · acknowledged · pendingallEveryone, only those who confirmed, or only those still pending.
departmenttexto · varios separados por comasOnly people in these departments, by name.
centertexto · varios separados por comasOnly people at these sites, by name.
sorttextoname · acknowledged_atnameField to sort by. Defaults to name.
ordertextoasc · descascSort direction.

GET /metrics

metrics:read

La plantilla de hoy, las altas, las publicaciones y el alcance por publicación y por departamento. Los mismos números que la pantalla de analítica, calculados por el mismo código.

En la consulta

ParámetroTipoValores admitidosPor defectoEjemploQué hace
period_daysenteromínimo 1 · máximo 3653090How many days back the period covers. Applies to every figure with a period, including the reach lists.
includetexto · varios separados por comasworkforce · announcements · reach_by_announcement · reach_by_departmentworkforce,announcementsWhich blocks to compute, comma-separated. Leave it out for all of them. `reach_by_announcement` is the expensive one: skip it if your dashboard only draws the headline figures.

GET /webhooks

webhooks:read

Vuestras direcciones registradas, cada una con su salud: fallos seguidos, qué contestó vuestro servidor la última vez y, si la hemos pausado, por qué.

En la consulta

ParámetroTipoValores admitidosPor defectoEjemploQué hace
pageenteromínimo 112Page to return. The first page is 1, not 0.
per_pageenteromínimo 1 · máximo 200 · fuera de rango se recorta en vez de fallar50100How many items per page. Anything above 200 is clamped to 200 rather than rejected: asking for more is a reasonable thing to want.
eventtextoannouncement.published · announcement.pending_review · announcement.acknowledged · person.created · person.deactivatedOnly endpoints subscribed to this event.
activebooleanoOnly live endpoints, or only the switched-off and paused.

POST /webhooks

webhooks:write

Registra una dirección y los sucesos que quiere. La respuesta trae el secreto de firma, y es la única vez que se ve.

En el cuerpo

ParámetroTipoValores admitidosPor defectoEjemploQué hace
urlObligatoriotextolargo máximo 500https://hooks.example.com/blastackWhere we POST. https only, no credentials, no fragment.
eventsObligatoriolistaannouncement.published · announcement.pending_review · announcement.acknowledged · person.created · person.deactivated["announcement.published"]Which events this endpoint wants. At least one.
nametextolargo máximo 120Zapier · switchboardWhat it is for, in your words. It is the only thing that lets you delete the right one six months from now.
secrettextolargo máximo 200Your own signing secret, if you would rather set it than have one generated. At least 32 characters.
activebooleanotrueStart delivering straight away.

GET /webhooks/{id}

webhooks:read

Una dirección concreta, con su salud.

En la dirección

ParámetroTipoValores admitidosPor defectoEjemploQué hace
idObligatoriotextoThe webhook endpoint id.

PATCH /webhooks/{id}

webhooks:write

Cambia lo que mandéis. Poner la dirección otra vez activa levanta también la pausa que hayamos aplicado tras fallos repetidos y pone el contador a cero.

En la dirección

ParámetroTipoValores admitidosPor defectoEjemploQué hace
idObligatoriotextoThe webhook endpoint id.

En el cuerpo

ParámetroTipoValores admitidosPor defectoEjemploQué hace
urltextolargo máximo 500Where we POST. https only.
eventslistaannouncement.published · announcement.pending_review · announcement.acknowledged · person.created · person.deactivatedWhich events this endpoint wants. At least one.
nametextolargo máximo 120What it is for.
activebooleanoSwitch delivery on or off. Turning it on clears a pause.
rotate_secretbooleanoGenerate a new signing secret and return it once in this reply.

DELETE /webhooks/{id}

webhooks:write

La borra con su histórico. Para dejar de recibir un tiempo sin perder el histórico, es mejor desactivarla.

En la dirección

ParámetroTipoValores admitidosPor defectoEjemploQué hace
idObligatoriotextoThe webhook endpoint id.

GET /webhooks/{id}/deliveries

webhooks:read

Todo lo que hemos intentado mandaros: intentos, qué contestasteis y cuándo toca el siguiente. El identificador es el mismo que viajó en la cabecera, así que se puede cruzar con vuestros registros.

En la dirección

ParámetroTipoValores admitidosPor defectoEjemploQué hace
idObligatoriotextoThe webhook endpoint id.

En la consulta

ParámetroTipoValores admitidosPor defectoEjemploQué hace
pageenteromínimo 112Page to return. The first page is 1, not 0.
per_pageenteromínimo 1 · máximo 200 · fuera de rango se recorta en vez de fallar50100How many items per page. Anything above 200 is clamped to 200 rather than rejected: asking for more is a reasonable thing to want.
statustextopending · delivered · given_upOnly deliveries in this state.
eventtextoannouncement.published · announcement.pending_review · announcement.acknowledged · person.created · person.deactivatedOnly deliveries of this event type.
sincefecha ISO 8601Only events that happened at or after this instant.

Ejemplos

Tres cosas que una fábrica automatiza el primer mes

Con su petición entera y su respuesta entera. Cambiad la clave y los nombres; lo demás funciona tal cual.

Cuando el sistema de mantenimiento cierra la orden de parada

El ERP registra una parada de línea y el tablón se entera solo

Es el caso que se pide siempre. Vuestro sistema ya sabe que la línea 3 para el jueves; con esta llamada lo publica él, dirigido a los departamentos que toca y pidiendo confirmación de lectura, sin que nadie escriba nada dos veces.

Petición

curl -X POST https://app.blastack.com/api/v1/announcements \
  -H "Authorization: Bearer blsk_a1b2c3d4e5f6_YOUR_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
       "author": "EMP-0042",
       "title": "Parada de la línea 3 el jueves",
       "body": "El jueves 4 a las 06:00 para la línea 3 por mantenimiento programado. El turno de mañana entra a la hora de siempre y pasa a la línea 2.",
       "audience": {
         "type": "departments",
         "ids": [
           "Mantenimiento",
           "Calidad"
         ]
       },
       "requires_acknowledgement": true,
       "notify": true,
       "language": "es"
     }'

Respuesta · 201

{
  "id": "6b1f0f1e-0c2a-4c5e-9a3d-2f5b8c7d1e40",
  "title": "Parada de la línea 3 el jueves",
  "body": "El jueves 4 a las 06:00 para la línea 3 por mantenimiento programado. El turno de mañana entra a la hora de siempre y pasa a la línea 2.",
  "status": "published",
  "kind": "news",
  "language": "es",
  "audience": {
    "type": "departments",
    "ids": [
      "9c1d7e5f-2a3b-4c5d-8e9f-0a1b2c3d4e5f",
      "44ab7e5f-2a3b-4c5d-8e9f-0a1b2c3d4e5f"
    ],
    "names": [
      "Mantenimiento",
      "Calidad"
    ]
  },
  "community": null,
  "section": null,
  "author": {
    "id": "3f2a1b0c-9d8e-4f7a-8b6c-5d4e3f2a1b0c",
    "name": "Ana García",
    "email": "ana.garcia@empresa.com",
    "external_id": "EMP-0042"
  },
  "notify": true,
  "comments_enabled": true,
  "pinned": false,
  "acknowledgements": {
    "required": true,
    "confirmed": 0
  },
  "reads": 0,
  "published_at": "2026-09-01T06:00:00.000Z",
  "created_at": "2026-09-01T06:00:00.000Z",
  "updated_at": "2026-09-01T06:00:00.000Z",
  "url": "https://app.blastack.com/inicio#post-6b1f0f1e-0c2a-4c5e-9a3d-2f5b8c7d1e40"
}

El destino va por nombre y no por identificador, porque es lo que vuestro sistema tiene a mano. Si el nombre no existe, la respuesta dice cuál era y no publica nada: media publicación sería peor.

Cuando el sistema de personal marca la baja

Alguien sale del sistema de personal y queda fuera el mismo día

Si vuestro sistema habla SCIM, esto ya lo hace solo y no hace falta esta llamada. Si no lo habla —que es lo normal en una fábrica—, con una línea queda desactivado y con las sesiones cerradas en el acto. La dirección admite vuestro propio identificador, así que no hay que mantener una tabla de equivalencias.

Petición

curl -X DELETE https://app.blastack.com/api/v1/people/EMP-0042 \
  -H "Authorization: Bearer blsk_a1b2c3d4e5f6_YOUR_SECRET"

Respuesta · 200

{
  "id": "3f2a1b0c-9d8e-4f7a-8b6c-5d4e3f2a1b0c",
  "external_id": "EMP-0042",
  "email": "ana.garcia@empresa.com",
  "name": "Ana García",
  "job_title": "Jefa de turno",
  "department": "Mantenimiento",
  "center": "Nave 2",
  "role": "editor",
  "status": "disabled",
  "language": null,
  "created_at": "2026-02-11T08:14:00.000Z",
  "updated_at": "2026-09-01T07:40:22.000Z"
}

Desactiva; no borra. Lo que publicó se queda, porque un tablón lleno de huecos no ayuda a nadie. Llamar dos veces no es un error.

Cada mañana, o el día antes de una auditoría

Quién ha confirmado el protocolo, a una hoja de cálculo

Un escenario de Make o un flujo de n8n que pida los pendientes y los escriba en una hoja: nombre, departamento y si ha confirmado. Es el informe que se pide en una auditoría y el que evita perseguir a nadie por el pasillo.

Petición

curl -G https://app.blastack.com/api/v1/announcements/6b1f0f1e-0c2a-4c5e-9a3d-2f5b8c7d1e40/acknowledgements \
  --data-urlencode "state=pending" \
  --data-urlencode "department=Mantenimiento" \
  --data-urlencode "per_page=200" \
  -H "Authorization: Bearer blsk_a1b2c3d4e5f6_YOUR_SECRET"

Respuesta · 200

{
  "announcement": {
    "id": "6b1f0f1e-0c2a-4c5e-9a3d-2f5b8c7d1e40",
    "title": "Parada de la línea 3 el jueves"
  },
  "summary": {
    "recipients": 34,
    "acknowledged": 27,
    "pending": 7
  },
  "data": [
    {
      "person": {
        "id": "3f2a1b0c-9d8e-4f7a-8b6c-5d4e3f2a1b0c",
        "name": "Ana García",
        "email": "ana.garcia@empresa.com",
        "external_id": "EMP-0042",
        "department": "Mantenimiento",
        "center": "Nave 2"
      },
      "acknowledged": false,
      "acknowledged_at": null
    }
  ],
  "page": 1,
  "per_page": 200,
  "total": 7
}

El resumen se cuenta sobre la población entera y no sobre la página ni sobre el filtro, para que el «12 de 30» siga significando algo. La hora de la confirmación es la de la primera vez y no se toca nunca más.

Avisos salientes

Llamamos nosotros, en menos de un minuto

Es lo que convierte esto en algo conectable de verdad. Sin avisos salientes, el disparador de vuestro escenario tiene que ser «cada cinco minutos, pregunta», y eso es lento, se paga por pasada y casi nunca encuentra nada.

SucesoCuándo salta
announcement.publishedAlgo ha salido al muro, venga por donde venga: el compositor, el editor, esta API, la cola de revisión o una publicación programada que llega a su hora. El tipo va dentro, por si solo os interesan los avisos y no las encuestas o los eventos.
announcement.pending_reviewUna publicación espera aprobación. Solo salta en empresas que tienen encendida la cola de revisión.
announcement.acknowledgedAlguien ha confirmado una lectura obligatoria. Un suceso por persona, con quién y cuándo.
person.createdSe ha dado de alta a alguien, da igual por dónde: aprovisionamiento, código de acceso, inicio de sesión único, la pantalla de administración o esta API.
person.deactivatedSe ha desactivado a alguien. Como aquí no se borra a nadie, esta es la señal de salida.

El sobre, igual en los cinco

{
  "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
  "type": "announcement.published",
  "occurred_at": "2026-09-01T06:00:00.000Z",
  "organization_id": "0a9b8c7d-6e5f-4a3b-8c9d-0e1f2a3b4c5d",
  "data": {
    "announcement": {
      "id": "6b1f0f1e-0c2a-4c5e-9a3d-2f5b8c7d1e40",
      "title": "Parada de la línea 3 el jueves",
      "body": "El jueves 4 a las 06:00 para la línea 3 por mantenimiento programado.",
      "kind": "news",
      "language": "es",
      "requires_acknowledgement": true,
      "audience": {
        "type": "departments",
        "names": [
          "Mantenimiento",
          "Calidad"
        ]
      },
      "community": "Planta de Getafe",
      "author": {
        "id": "3f2a1b0c-9d8e-4f7a-8b6c-5d4e3f2a1b0c",
        "name": "Ana García",
        "email": "ana.garcia@empresa.com",
        "external_id": "EMP-0042"
      },
      "url": "https://app.blastack.com/inicio#post-6b1f0f1e-0c2a-4c5e-9a3d-2f5b8c7d1e40",
      "status": "published"
    }
  }
}

Lo de dentro cambia con el suceso; el sobre no. El identificador no cambia entre reintentos, así que descartar un repetido es un condicional de una línea en vuestro lado.

Las cabeceras de cada envío

CabeceraPara qué
blastack-eventQué ha pasado. Es el valor por el que se ramifica.
blastack-event-idEl identificador del suceso. No cambia entre reintentos: es el que sirve para no ejecutar dos veces lo mismo.
blastack-delivery-attemptQué intento es este, empezando por el primero.
blastack-signatureLa firma. Es lo único que separa un aviso nuestro de uno que se haya inventado quien descubra vuestra dirección.

Comprobar la firma

La firma es HMAC-SHA256 sobre la marca de tiempo, un punto y el cuerpo en crudo, sin parsear. Es el mismo formato que usan las pasarelas de pago, o sea el que probablemente ya habéis escrito una vez.

import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(rawBody, headers, secret) {
  const sent = headers["blastack-signature"] ?? "";
  const t = /t=(\d+)/.exec(sent)?.[1];
  const v1 = /v1=([0-9a-f]+)/.exec(sent)?.[1];
  if (!t || !v1) return false;

  const mine = createHmac("sha256", secret)
    .update(`${t}.${rawBody}`, "utf8")
    .digest("hex");

  if (v1.length !== mine.length) return false;
  if (!timingSafeEqual(Buffer.from(v1, "hex"), Buffer.from(mine, "hex"))) {
    return false;
  }
  return Math.abs(Date.now() / 1000 - Number(t)) < 300;
}

Comparad en tiempo constante y rechazad lo que tenga una marca de más de unos minutos. Sin esa segunda comprobación, una copia de un envío antiguo se os puede reenviar durante años y la firma seguirá siendo válida.

Reintentos, pausa y por qué no llega nada

  • Vale cualquier respuesta 2xx. No leemos el cuerpo, así que contestad rápido y haced el trabajo después: a los diez segundos abandonamos el intento y cuenta como fallo.
  • Seis intentos con espera creciente —un minuto, cinco, quince, una hora, tres y seis— repartidos en unas diez horas y media. Cubre el despliegue de noche y el mantenimiento de fin de semana.
  • Treinta fallos seguidos pausan la dirección, y queda escrito el porqué. Se levanta volviéndola a poner activa, que además pone el contador a cero.
  • Solo llamamos a direcciones https públicas. Se comprueba al registrarlas y otra vez antes de cada envío, porque un nombre que hoy resuelve a una dirección de internet puede resolver mañana a una de vuestra red interna.
  • Una dirección registrada hoy no recibe lo de ayer: una suscripción empieza a contar desde que se crea, para que montar el primer escenario no dispare cuarenta ejecuciones de golpe.

Errores

Siempre la misma forma

Todo error devuelve el mismo objeto, con un código pensado para que le escribáis un condicional encima. Los códigos no se renombran nunca: se añaden. El mensaje de al lado es para quien lee el registro y ese sí puede cambiar.

{
  "error": {
    "status": 403,
    "code": "insufficient_scope",
    "message": "This API key does not have the \"metrics:read\" scope.",
    "required_scope": "metrics:read",
    "granted_scopes": [
      "people:read"
    ]
  },
  "documentation": "https://blastack.com/en/developers"
}

Un parámetro desconocido se rechaza, no se ignora. Un filtro que se descarta en silencio os devuelve más datos de los que pedisteis y nada os avisa.

RateLimit-Limit · RateLimit-Remaining · RateLimit-Reset · Retry-After

CódigoCuándo sale
invalid_token401 · Falta la clave, está mal escrita o se ha revocado. No decimos cuál de las tres.
insufficient_scope403 · La clave es buena pero no tiene el ámbito que pide esa llamada. La respuesta dice cuál hacía falta y cuáles tiene.
module_required403 · La empresa no tiene contratado el módulo de identidad corporativa, que es al que pertenece la API.
read_only403 · La prueba gratuita ha terminado y la empresa está en solo lectura. Solo afecta a lo que escribe.
rate_limited429 · Más peticiones de la cuenta en un minuto. La respuesta dice cuántos segundos hay que esperar.
invalid_request400 · Falta un campo, sobra o no se entiende. El mensaje nombra el parámetro exacto y lista los valores que admite.
not_found404 · No hay nada vuestro con ese identificador. Es también lo que se recibe por algo de otra empresa: distinguirlo sería una forma de averiguar qué existe fuera.
conflict409 · Choca con algo que ya está: dos personas con el mismo correo, dos direcciones de aviso iguales. La respuesta trae el identificador de lo que ya existía, para poder seguir.
server_error500 · Se ha roto algo de nuestro lado. Nada del error sale en la respuesta; entero se queda en nuestro registro.

Límites

Lo que conviene saber antes de escribir el bucle

  • Ciento veinte peticiones por minuto y clave. Cada respuesta lleva cuántas quedan y en cuántos segundos se renueva la cuenta; un 429 lleva además cuánto esperar.
  • Las listas vienen de cincuenta en cincuenta y admiten hasta doscientas por página. La primera página es la uno, no la cero. Pedir más de doscientas se recorta en vez de fallar.
  • El periodo de los indicadores llega hasta trescientos sesenta y cinco días y se pide por parámetro. El bloque de alcance por publicación es el caro: se puede dejar fuera si vuestro cuadro de mando solo pinta las cifras de cabecera.
  • Veinte direcciones de aviso saliente por empresa. No es un límite comercial: cada suceso se encola una vez por dirección, y quien necesite más está repartiendo en el sitio equivocado.
  • Los avisos salientes tardan hasta un minuto: se detectan leyendo la base y no desde el botón de publicar, para que el servidor lento de una integración no pueda frenar el tablón de nadie.
  • Solo se anuncia lo de las últimas veinticuatro horas. Si nuestra tarea del reloj estuviera parada un día entero, no os soltamos el trimestre de golpe: vuestras automatizaciones se ejecutarían con datos de anteayer como si fueran de ahora.

Lo que no hace

Dicho aquí para que no lo descubráis a mitad

Quien va a integrar necesita saber los límites antes de empezar, no cuando ya lleva medio escenario montado. Esta lista es lo que un desarrollador va a buscar y no va a encontrar, y se actualiza cuando algo deja de ser cierto.

  • No hay pantalla para crear claves. Hoy se emiten por consola con npm run clave:api, y las emitimos nosotros. Una pantalla honesta necesitaría sus textos en los doce idiomas de la aplicación, y media pantalla traducida no se publica.
  • Los avisos salientes necesitan una línea de cron en el servidor. Sin ella no se rompe nada, y ese es el problema: todo lo demás sigue funcionando y sencillamente no sale nada. Se ve en el registro de entregas, que estaría vacío.
  • El secreto de firma se enseña una sola vez: al crear la dirección o al rotarlo. No se puede volver a leer, solo rotar. Guardadlo en ese momento.
  • Rotar el secreto no tiene periodo de gracia. El envío siguiente ya va firmado con el nuevo, así que hay que cambiarlo primero en vuestro servidor y pedir la rotación después.
  • Veinte direcciones de aviso saliente por empresa. Si os hacen falta más, el reparto lo tiene que hacer vuestra herramienta a partir de una sola entrega.
  • No se llega al chat, ni a las encuestas, ni a los eventos, ni a los reconocimientos, ni a los álbumes. Salen en la lista de publicaciones y en los avisos salientes con su tipo, pero no se crean ni se leen por su cuenta.
  • No se llega a los documentos ni se suben ficheros. Ni imágenes en una publicación: el cuerpo es texto plano.
  • No se edita ni se borra una publicación ya hecha, ni se comenta, ni se lee un comentario.
  • No se leen personas de una en una por correo con búsqueda parcial, ni se borra a nadie de verdad: borrar es una petición por escrito que atendemos nosotros, y anonimiza en vez de eliminar.
  • Las métricas son agregadas y no hay forma de sacar quién ha leído qué. La única lista con nombres es la de las lecturas obligatorias, que existe para poder enseñársela a una inspección.
  • No hay entorno de pruebas separado, ni bibliotecas oficiales para ningún lenguaje, ni paginación por cursor. Es JSON sobre HTTPS y se llama con lo que tengáis.

Para máquinas

Si esto lo está leyendo un asistente

Media evaluación de compra la hace hoy alguien preguntándole a un asistente si esto se conecta con algo. Así que la respuesta está escrita para que se pueda leer sin cuenta y sin adivinar nada.

  • La especificación OpenAPI 3.1 está en una dirección pública y estable, sin clave, con permiso de origen cruzado y sin tocar la base de datos. Describe los diecisiete extremos con sus parámetros, sus valores admitidos, la forma de cada respuesta y los cinco sucesos salientes.
  • No está escrita a mano: se genera del mismo catálogo con el que cada extremo comprueba lo que le llega, y las formas de respuesta se sacan ejecutando el mismo código que atiende las peticiones. No puede decir algo distinto de lo que hace.
  • Esta página tiene la misma estructura en los seis idiomas, con encabezados que dicen lo que hay debajo y tablas de verdad, no imágenes ni columnas fingidas.
  • Hay además un llms.txt con la versión corta, con la sección de integración escrita aparte y con las direcciones y los ámbitos en texto plano.

Si os falta algo de esa lista, decidlo

Lo que más se pida es lo siguiente que se escribe. Y si algo de esta página no cuadra con lo que responde el servidor, es un fallo nuestro y queremos saberlo el mismo día.