Blastack

Developers

The API, and the three tools it plugs into

Seventeen operations across ten paths, five outgoing events and a public OpenAPI 3.1 spec that Make and n8n import on their own. It is all here: every parameter one by one, examples that work as they stand, and the list of what it does not do.

Paste the first URL into the n8n or Make importer and the nodes appear by themselves. Zapier does not read OpenAPI, which is exactly why its steps are written out separately below.

At a glance

What the first call needs

Eight facts. With these you can write the request without reading any further.

WhatValueNote
API roothttps://app.blastack.com/api/v1It lives in the application, not on this marketing site. One address, and it does not change.
Specificationhttps://app.blastack.com/api/openapi.jsonPublic and unauthenticated, so an importer can read it before you have any credentials.
AuthenticationAuthorization: Bearer blsk_<prefix>_<secret>In the header, never in the URL. URLs end up in the server log, in the proxy log in front of it and in the shell history of whoever tried it by hand.
Formatapplication/json; charset=utf-8JSON in, JSON out. There is no XML and no form encoding.
Rate limit120 req/min · RateLimit-RemainingPer key, per minute. Every reply says how many are left and when the window resets.
Pagingpage=1 · per_page=50 · ≤ 200The first page is 1, not 0. Asking for more than two hundred is clamped rather than refused.
Errors{ error: { status, code, message } }Always the same object, with a stable code meant to be branched on.
Sandbox—There is none. You work against production, which is why it pays to start with a read-only key.
curl https://app.blastack.com/api/v1/people?per_page=1 \
  -H "Authorization: Bearer blsk_a1b2c3d4e5f6_YOUR_SECRET"

What exists

Three surfaces, none of them half-built

All three are running. What is missing is further down, in its own section and named out loud.

SCIM 2.0 provisioning

So your HR system creates people, moves them between departments and deactivates them without anyone retyping it. It is the half that actually matters to an IT department: the day you let someone go, the account deactivates itself.

Filtering by userName and externalId, paging and partial update. Department and work address are read, so whoever arrives this way is born with their department and site already set. Deprovisioning deactivates; it does not delete.

Single sign-on (OpenID Connect)

Signing in with the company account from Entra ID, Google Workspace or Okta. It lives alongside the join code, which is how the part of the workforce with no work email gets in.

Configured by pasting the provider's discovery URL, and checked against it before saving. Email domains are verified over DNS. There is no SAML and no LDAP.

The Blastack API

Posting from the system that already knows line 3 stops on Thursday, deactivating whoever leaves the HR system, and being called by us when something happens instead of having to ask every five minutes.

JSON over HTTPS, an API key in a header, scopes per key, errors with a stable code and signed outgoing events. It is what the rest of this page documents.

Automation

Zapier, Make and n8n, step by step

The three are not set up the same way, and saying so saves the afternoon you would spend finding out. Make and n8n read the specification and almost configure themselves; Zapier does not read it, so there every call is written by hand. All three work; the work is not the same.

ToolReads the spec?Recommended trigger
n8nYes, the whole thingWebhook node + POST /webhooks
MakeYes, in the custom apps editorCustom webhook + POST /webhooks
ZapierNoCatch Hook + POST /webhooks

n8n

The shortest of the three. Create a header credential, paste the spec URL into the importer, and you end up with one node per operation with its parameters already in place.

Steps

  1. 01Credentials → new → Header Auth. The header name is Authorization and the value is Bearer followed by a space and your key.
  2. 02HTTP Request node, authentication Generic Credential Type → Header Auth, then the credential you just made. Method and path come from the endpoint table.
  3. 03To avoid typing the parameters, feed the importer the spec URL: it describes every endpoint with its accepted values, so what gets generated comes with dropdowns instead of free-text boxes.
  4. 04To trigger: Webhook node, copy its production URL and register it with POST /webhooks choosing the events. Polling every five minutes stops being necessary.

Worth knowing

The webhook node answers 200 before the rest of the flow runs, which is exactly what is needed here: we give up after ten seconds and that counts as a failure.

Make

The generic HTTP module covers everything, and anyone about to build many scenarios can upload the spec to the custom apps editor and get one module per operation.

Steps

  1. 01HTTP module → Make an API Key Auth request. In the connection, the key is called Authorization, the value is Bearer plus your key, and it goes in the header.
  2. 02Method, path and body come from the endpoint table and from the examples below. The content type is application/json.
  3. 03If you are building several scenarios, the custom apps editor accepts the specification and generates one module per operation: fewer hand-written scenarios and fewer typos in filter names.
  4. 04To trigger: Webhooks module → Custom webhook, copy its URL and register it with POST /webhooks. Make answers immediately and processes afterwards, which is the right way round here.

Worth knowing

Make charges per operation: a scenario that polls every five minutes burns eight thousand operations a month finding almost nothing. With the webhook you spend one per event.

Zapier

Zapier does not import OpenAPI. You build it with the generic webhook steps, or by making a private integration on their developer platform, where every endpoint is declared by hand.

Steps

  1. 01Trigger: Webhooks by Zapier → Catch Hook. Copy the URL it gives you and register it with POST /webhooks choosing the events.
  2. 02Action: Webhooks by Zapier → Custom Request. Method and URL from the table, headers Authorization with Bearer plus your key and Content-Type with application/json, and the JSON in the data field.
  3. 03Add a Code by Zapier step that verifies the signature before doing anything. Zapier does not verify it for you and the trigger URL is public: without that step, anyone who finds it can invent a line stoppage.
  4. 04If you would rather have named nodes than generic requests, you have to build a private integration and declare every endpoint with its fields there. It is more work, and the specification does not save you any of it.

Worth knowing

Their polling triggers spend a task on every pass, whether or not there was anything. Catch Hook only spends when something actually happened.

Keys

One key per integration, carrying only what it needs

Today we issue them by hand and tell you which scopes they carry. There is no self-service screen and that is said below, among what is missing. Every key is tied to one company, and no route reads a company id from the request, so there is nowhere to get it wrong.

  1. 01

    Ask us for the key

    Tell us what it is for and what it has to be able to do, and we issue it with those scopes and no others. It is shown once: we keep a digest of it and nothing else, so a lost key is revoked and reissued.

  2. 02

    Send it on every request

    In the authorization header, never in the URL. URLs end up in the server log, in the proxy log in front of it and in the shell history of whoever tried it by hand.

  3. 03

    Store it where you store the others

    In your secret manager, not in the repository. It starts with a recognisable prefix precisely so that secret scanners catch it if it escapes.

The API belongs to the corporate identity module, the same one that opens single sign-on and provisioning. If the company has not enabled it, the reply says so in those words rather than with a generic error.

The scopes, one by one

ScopeWhat it opens
people:readReading the workforce and the departments and sites an announcement can be aimed at. It is all a dashboard needs.
people:writeCreating, updating and deactivating people. Deactivating closes their sessions, so it does not come free with the read scope.
announcements:readReading what has been posted and who confirmed a required reading. It is separate from the people scope: a list of names and job titles is the directory, and the text of the announcements is not.
announcements:writePosting to the wall. It respects the company's review queue, so a key cannot jump anyone's approval circuit.
metrics:readThe aggregated figures. Nobody is named here.
webhooks:readSeeing the registered endpoints and the delivery log. It is what answers 'why is nothing arriving' without opening a ticket.
webhooks:writeRegistering, updating and deleting outgoing endpoints. The most delicate of the list: it decides which server on the internet we send the content of your announcements to.

Endpoints

Seventeen operations across ten paths

Grouped by family. The scope column is the one to look at when asking for a key: if it is missing, the reply says which one was needed and which ones yours carries.

MethodPathScopeWhat it does
GET/peoplepeople:readThe workforce, with department, site and the id each person has in your HR system. Seventeen parameters, among them a filter by last modification date, which is the one that lets you sync every quarter of an hour without pulling everything.
GET/people/{id}people:readOne person, looked up by our id or by yours. Useful to refresh a record without walking the whole list.
POST/peoplepeople:writeAdds someone. It is for when your HR system does not speak SCIM; if it does, prefer SCIM, which also removes people. Department and site are given by name and created if they do not exist.
PATCH/people/{id}people:writeChanges only the fields you send. One you leave out is not touched, so a sync that only knows the job title cannot wipe anyone's site.
DELETE/people/{id}people:writeDeactivates; it never deletes. Sessions are closed straight away, so someone let go on Friday is out on Friday. What they posted stays.
GET/audiencespeople:readThe departments and sites that exist, with how many people are in each. It is where the names an announcement can be aimed at come from, and the empty-group filter is the one that finds a broken provisioning mapping.
GET/announcementsannouncements:readEverything posted, with the reads and confirmations on each item. Drafts and rejected posts are included so a dashboard can draw the whole funnel.
GET/announcements/{id}announcements:readOne announcement. It is the next step after the id returned by a post or carried by an outgoing event.
POST/announcementsannouncements:writePosts to the wall. You must say who signs it and there is no default. If that person cannot publish without review, it joins the queue and the reply says so.
GET/announcements/{id}/acknowledgementsannouncements:readWho confirmed a required reading and who has not, with name and time. It is the only named list in the whole API, and it is named because that is precisely the point of required reading: a voluntary, signed, timed act you can show an inspector.
GET/metricsmetrics:readToday's headcount, the joiners, the posts and the reach by announcement and by department. The same numbers as the analytics screen, computed by the same code.
GET/webhookswebhooks:readYour registered endpoints, each with its health: consecutive failures, what your server last answered and, if we paused it, why.
POST/webhookswebhooks:writeRegisters an endpoint and the events it wants. The reply carries the signing secret, and that is the only time you will see it.
GET/webhooks/{id}webhooks:readOne endpoint, health included.
PATCH/webhooks/{id}webhooks:writeChanges what you send. Setting it active again also clears a pause we applied after repeated failures and resets the failure count.
DELETE/webhooks/{id}webhooks:writeRemoves it along with its history. To stop deliveries for a while without losing the history, switch it off instead.
GET/webhooks/{id}/deliverieswebhooks:readEverything we have tried to send you: attempts, what you answered and when the next one is due. The id is the same one that travelled in the header, so it cross-references with your own logs.

Parameters

All of them, endpoint by endpoint

No list here is abridged: these are the same ones the server checks. A parameter that is not here does not exist, and writing it returns an error naming the ones that endpoint takes instead of dropping it silently.

The right-hand column is in English on purpose. It comes from the catalogue the specification is also generated from, so it is word for word what n8n and Make show inside their nodes. Translating it here would mean two versions that would one day say different things.

GET /people

people:read

The workforce, with department, site and the id each person has in your HR system. Seventeen parameters, among them a filter by last modification date, which is the one that lets you sync every quarter of an hour without pulling everything.

In the query string

ParameterTypeAccepted valuesDefaultExampleWhat it does
pageintegermin 112Page to return. The first page is 1, not 0.
per_pageintegermin 1 · max 200 · out of range is clamped, not refused50100How many items per page. Anything above 200 is clamped to 200 rather than rejected: asking for more is a reasonable thing to want.
sortstringcreated_at · updated_at · name · emailcreated_atField to sort by. Defaults to created_at.
orderstringasc · descascSort direction.
fieldsstring · several, comma-separatedexternal_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.
statusstring · several, comma-separatedactive · invited · disabledactiveOnly people in these states. Several may be given.
rolestring · several, comma-separatedowner · admin · gestor_personas · responsable_local · informatico · editor · revisor · analista · employeeOnly people with these roles. Several may be given.
departmentstring · several, comma-separatedMaintenance,QualityDepartment name, or several separated by commas. Case-insensitive: your directory writing MANTENIMIENTO is not an error.
centerstring · several, comma-separatedSite name, or several separated by commas. Case-insensitive.
no_departmentbooleanOnly 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.
languagestring · several, comma-separatedes · ca · en · pt · fr · it · de · sv · nl · pl · ro · csOnly people who picked this reading language.
emailstringExact email address, case-insensitive.
external_idstringExact external_id, as sent by your HR system over SCIM.
qstringgarciaFree text over name, email and job title. Case-insensitive, matches anywhere in the value.
updated_sinceISO 8601 date2026-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_sinceISO 8601 dateOnly people added at or after this instant.
created_untilISO 8601 dateOnly people added before this instant.

GET /people/{id}

people:read

One person, looked up by our id or by yours. Useful to refresh a record without walking the whole list.

In the path

ParameterTypeAccepted valuesDefaultExampleWhat it does
idRequiredstringa3f1…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.

In the query string

ParameterTypeAccepted valuesDefaultExampleWhat it does
fieldsstring · several, comma-separatedexternal_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

Adds someone. It is for when your HR system does not speak SCIM; if it does, prefer SCIM, which also removes people. Department and site are given by name and created if they do not exist.

In the body

ParameterTypeAccepted valuesDefaultExampleWhat it does
nameRequiredstringmax length 120Ana GarcíaFull name, as it should appear on the wall.
emailstringmax length 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_idstringmax length 200EMP-0042The id this person has in your system. Store it and you can cross both lists later without matching names.
job_titlestringmax length 120Job title.
departmentstringmax length 80Department name. Created if it does not exist. Without one, this person only ever sees company-wide announcements.
centerstringmax length 80Site name. Created if it does not exist.
rolestringadmin · 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.
statusstringactive · 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.
languagestringes · 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

Changes only the fields you send. One you leave out is not touched, so a sync that only knows the job title cannot wipe anyone's site.

In the path

ParameterTypeAccepted valuesDefaultExampleWhat it does
idRequiredstringa3f1…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.

In the body

ParameterTypeAccepted valuesDefaultExampleWhat it does
namestringmax length 120Full name.
emailstringmax length 200Work email.
external_idstringmax length 200The id this person has in your system.
job_titlestringmax length 120Job title.
departmentstringmax length 80Department name. Created if it does not exist.
centerstringmax length 80Site name. Created if it does not exist.
rolestringadmin · gestor_personas · responsable_local · informatico · editor · revisor · analista · employeeWhat they can do. `owner` cannot be given from the API.
statusstringactive · invited · disabledTheir state. `disabled` also closes their sessions.
languagestringes · ca · en · pt · fr · it · de · sv · nl · pl · ro · csReading language.

DELETE /people/{id}

people:write

Deactivates; it never deletes. Sessions are closed straight away, so someone let go on Friday is out on Friday. What they posted stays.

In the path

ParameterTypeAccepted valuesDefaultExampleWhat it does
idRequiredstringa3f1…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

The departments and sites that exist, with how many people are in each. It is where the names an announcement can be aimed at come from, and the empty-group filter is the one that finds a broken provisioning mapping.

In the query string

ParameterTypeAccepted valuesDefaultExampleWhat it does
typestringall · departments · centersallReturn only departments, only sites, or both.
qstringFree text over the name. Case-insensitive.
with_peoplebooleanOnly 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.
sortstringname · peoplenameField to sort by. Defaults to name.
orderstringasc · descascSort direction.

GET /announcements

announcements:read

Everything posted, with the reads and confirmations on each item. Drafts and rejected posts are included so a dashboard can draw the whole funnel.

In the query string

ParameterTypeAccepted valuesDefaultExampleWhat it does
pageintegermin 112Page to return. The first page is 1, not 0.
per_pageintegermin 1 · max 200 · out of range is clamped, not refused50100How many items per page. Anything above 200 is clamped to 200 rather than rejected: asking for more is a reasonable thing to want.
sortstringpublished_at · created_at · updated_at · titlepublished_atField to sort by. Defaults to published_at.
orderstringasc · descascSort direction.
fieldsstring · several, comma-separatedtitle · 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.
statusstring · several, comma-separateddraft · pending_review · published · rejected · scheduled · archivedpublished,scheduledOnly these states. Several may be given. `scheduled` means it is written and waiting for its hour.
authorstringWho signs it: our id, their email or their external_id. Any of the three.
audience_typestringall · departments · centersOnly announcements aimed this way.
departmentstring · several, comma-separatedOnly announcements aimed at these departments, by name. Several may be given.
centerstring · several, comma-separatedOnly announcements aimed at these sites, by name.
communitystringOnly announcements in this community, by id or by name.
requires_acknowledgementbooleanOnly required reading, or only the rest. This is the filter a compliance report is built on.
pinnedbooleanOnly pinned announcements, or only the rest.
qstringFree text over title and body. Case-insensitive.
published_sinceISO 8601 datePublished at or after this instant.
published_untilISO 8601 datePublished before this instant.
created_sinceISO 8601 dateWritten at or after this instant.
updated_sinceISO 8601 dateChanged at or after this instant.

GET /announcements/{id}

announcements:read

One announcement. It is the next step after the id returned by a post or carried by an outgoing event.

In the path

ParameterTypeAccepted valuesDefaultExampleWhat it does
idRequiredstringThe announcement id.

In the query string

ParameterTypeAccepted valuesDefaultExampleWhat it does
fieldsstring · several, comma-separatedtitle · 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

Posts to the wall. You must say who signs it and there is no default. If that person cannot publish without review, it joins the queue and the reply says so.

In the body

ParameterTypeAccepted valuesDefaultExampleWhat it does
bodyRequiredstringmax length 8000The text. Plain text; line breaks are kept.
authorRequiredstringEMP-0042Who signs it: our id, their email or their external_id. Must be an active person in this company.
titlestringmax length 160Headline.
audienceobjectWho 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.typestringall · departments · centersallAim at everyone, at departments or at sites.
audience.idsarray["Maintenance","Quality"]Department or site names, or their ids. Required unless type is `all`.
communitystringWhich 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.
sectionstringWhich section it belongs to, by id or by name. It must belong to the same community.
publish_atISO 8601 date2026-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_acknowledgementbooleanfalseRequired 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.
notifybooleantrueWhether 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_enabledbooleantrueWhether people can comment on it.
pinnedbooleanfalseKeep it at the top of the wall.
languagestringes · 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

Who confirmed a required reading and who has not, with name and time. It is the only named list in the whole API, and it is named because that is precisely the point of required reading: a voluntary, signed, timed act you can show an inspector.

In the path

ParameterTypeAccepted valuesDefaultExampleWhat it does
idRequiredstringThe announcement id.

In the query string

ParameterTypeAccepted valuesDefaultExampleWhat it does
pageintegermin 112Page to return. The first page is 1, not 0.
per_pageintegermin 1 · max 200 · out of range is clamped, not refused50100How many items per page. Anything above 200 is clamped to 200 rather than rejected: asking for more is a reasonable thing to want.
statestringall · acknowledged · pendingallEveryone, only those who confirmed, or only those still pending.
departmentstring · several, comma-separatedOnly people in these departments, by name.
centerstring · several, comma-separatedOnly people at these sites, by name.
sortstringname · acknowledged_atnameField to sort by. Defaults to name.
orderstringasc · descascSort direction.

GET /metrics

metrics:read

Today's headcount, the joiners, the posts and the reach by announcement and by department. The same numbers as the analytics screen, computed by the same code.

In the query string

ParameterTypeAccepted valuesDefaultExampleWhat it does
period_daysintegermin 1 · max 3653090How many days back the period covers. Applies to every figure with a period, including the reach lists.
includestring · several, comma-separatedworkforce · 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

Your registered endpoints, each with its health: consecutive failures, what your server last answered and, if we paused it, why.

In the query string

ParameterTypeAccepted valuesDefaultExampleWhat it does
pageintegermin 112Page to return. The first page is 1, not 0.
per_pageintegermin 1 · max 200 · out of range is clamped, not refused50100How many items per page. Anything above 200 is clamped to 200 rather than rejected: asking for more is a reasonable thing to want.
eventstringannouncement.published · announcement.pending_review · announcement.acknowledged · person.created · person.deactivatedOnly endpoints subscribed to this event.
activebooleanOnly live endpoints, or only the switched-off and paused.

POST /webhooks

webhooks:write

Registers an endpoint and the events it wants. The reply carries the signing secret, and that is the only time you will see it.

In the body

ParameterTypeAccepted valuesDefaultExampleWhat it does
urlRequiredstringmax length 500https://hooks.example.com/blastackWhere we POST. https only, no credentials, no fragment.
eventsRequiredarrayannouncement.published · announcement.pending_review · announcement.acknowledged · person.created · person.deactivated["announcement.published"]Which events this endpoint wants. At least one.
namestringmax length 120Zapier · switchboardWhat it is for, in your words. It is the only thing that lets you delete the right one six months from now.
secretstringmax length 200Your own signing secret, if you would rather set it than have one generated. At least 32 characters.
activebooleantrueStart delivering straight away.

GET /webhooks/{id}

webhooks:read

One endpoint, health included.

In the path

ParameterTypeAccepted valuesDefaultExampleWhat it does
idRequiredstringThe webhook endpoint id.

PATCH /webhooks/{id}

webhooks:write

Changes what you send. Setting it active again also clears a pause we applied after repeated failures and resets the failure count.

In the path

ParameterTypeAccepted valuesDefaultExampleWhat it does
idRequiredstringThe webhook endpoint id.

In the body

ParameterTypeAccepted valuesDefaultExampleWhat it does
urlstringmax length 500Where we POST. https only.
eventsarrayannouncement.published · announcement.pending_review · announcement.acknowledged · person.created · person.deactivatedWhich events this endpoint wants. At least one.
namestringmax length 120What it is for.
activebooleanSwitch delivery on or off. Turning it on clears a pause.
rotate_secretbooleanGenerate a new signing secret and return it once in this reply.

DELETE /webhooks/{id}

webhooks:write

Removes it along with its history. To stop deliveries for a while without losing the history, switch it off instead.

In the path

ParameterTypeAccepted valuesDefaultExampleWhat it does
idRequiredstringThe webhook endpoint id.

GET /webhooks/{id}/deliveries

webhooks:read

Everything we have tried to send you: attempts, what you answered and when the next one is due. The id is the same one that travelled in the header, so it cross-references with your own logs.

In the path

ParameterTypeAccepted valuesDefaultExampleWhat it does
idRequiredstringThe webhook endpoint id.

In the query string

ParameterTypeAccepted valuesDefaultExampleWhat it does
pageintegermin 112Page to return. The first page is 1, not 0.
per_pageintegermin 1 · max 200 · out of range is clamped, not refused50100How many items per page. Anything above 200 is clamped to 200 rather than rejected: asking for more is a reasonable thing to want.
statusstringpending · delivered · given_upOnly deliveries in this state.
eventstringannouncement.published · announcement.pending_review · announcement.acknowledged · person.created · person.deactivatedOnly deliveries of this event type.
sinceISO 8601 dateOnly events that happened at or after this instant.

Examples

Three things a factory automates in the first month

With the whole request and the whole reply. Change the key and the names; the rest works as it stands.

When the maintenance system closes the stoppage order

The ERP logs a line stoppage and the wall finds out by itself

This is the one people always ask for. Your system already knows line 3 stops on Thursday; with this call it posts that itself, aimed at the right departments and asking for a read confirmation, without anyone typing anything twice.

Request

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": "Line 3 down on Thursday",
       "body": "Line 3 stops on Thursday the 4th at 06:00 for scheduled maintenance. The morning shift clocks in at the usual time and moves to line 2.",
       "audience": {
         "type": "departments",
         "ids": [
           "Maintenance",
           "Quality"
         ]
       },
       "requires_acknowledgement": true,
       "notify": true,
       "language": "en"
     }'

Reply · 201

{
  "id": "6b1f0f1e-0c2a-4c5e-9a3d-2f5b8c7d1e40",
  "title": "Line 3 down on Thursday",
  "body": "Line 3 stops on Thursday the 4th at 06:00 for scheduled maintenance. The morning shift clocks in at the usual time and moves to line 2.",
  "status": "published",
  "kind": "news",
  "language": "en",
  "audience": {
    "type": "departments",
    "ids": [
      "9c1d7e5f-2a3b-4c5d-8e9f-0a1b2c3d4e5f",
      "44ab7e5f-2a3b-4c5d-8e9f-0a1b2c3d4e5f"
    ],
    "names": [
      "Maintenance",
      "Quality"
    ]
  },
  "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"
}

The audience goes by name and not by id, because a name is what your system has to hand. If the name does not exist, the reply says which one it was and nothing is posted: half an announcement would be worse.

When the HR system flags the leaver

Someone leaves the HR system and is out the same day

If your system speaks SCIM this already happens on its own and you do not need this call. If it does not — which is the norm in a factory — one line deactivates them and closes their sessions on the spot. The path accepts your own id, so there is no mapping table to maintain.

Request

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

Reply · 200

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

It deactivates; it does not delete. What they posted stays, because a wall full of holes helps nobody. Calling it twice is not an error.

Every morning, or the day before an audit

Who confirmed the protocol, into a spreadsheet

A Make scenario or an n8n flow that asks for the pending ones and writes them into a sheet: name, department and whether they confirmed. It is the report an audit asks for, and the one that saves chasing anyone down a corridor.

Request

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

Reply · 200

{
  "announcement": {
    "id": "6b1f0f1e-0c2a-4c5e-9a3d-2f5b8c7d1e40",
    "title": "Line 3 down on Thursday"
  },
  "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": "Maintenance",
        "center": "Plant 2"
      },
      "acknowledged": false,
      "acknowledged_at": null
    }
  ],
  "page": 1,
  "per_page": 200,
  "total": 7
}

The summary is counted over the whole population, not over the page or the filter, so that '12 of 30' keeps meaning something. The confirmation time is the first one and is never touched again.

Outgoing events

We call you, within a minute

This is what makes it genuinely connectable. Without outgoing events, your scenario's trigger has to be 'every five minutes, ask', and that is slow, billed per pass and almost never finds anything.

EventWhen it fires
announcement.publishedSomething went live on the wall, whatever route it came by: the composer, the full editor, this API, the review queue or a scheduled post reaching its hour. The kind travels inside, in case you only want plain announcements and not surveys or events.
announcement.pending_reviewAn announcement is waiting for approval. It only fires in companies that have the review queue switched on.
announcement.acknowledgedSomeone confirmed a required reading. One event per person, carrying who and when.
person.createdSomeone was added, no matter how: provisioning, a join code, single sign-on, the admin screen or this API.
person.deactivatedSomeone was deactivated. Since nobody is ever deleted here, this is the offboarding signal.

The envelope, the same for all five

{
  "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"
    }
  }
}

What is inside changes with the event; the envelope does not. The id does not change across retries, so dropping a duplicate is a one-line condition on your side.

The headers on every delivery

HeaderWhat for
blastack-eventWhat happened. It is the value you branch on.
blastack-event-idThe event id. It does not change across retries: it is the one to key on so you never run the same thing twice.
blastack-delivery-attemptWhich attempt this is, starting at the first.
blastack-signatureThe signature. It is the only thing that separates a delivery of ours from one invented by whoever finds your URL.

Verifying the signature

The signature is HMAC-SHA256 over the timestamp, a dot and the raw body, unparsed. It is the same format the payment gateways use, which means the one you have probably written once already.

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;
}

Compare in constant time and reject anything whose timestamp is more than a few minutes old. Without that second check, a copy of an old delivery can be replayed at you for years and the signature will still verify.

Retries, pausing, and why nothing is arriving

  • Any 2xx will do. We do not read the body, so answer fast and do the work afterwards: we give up after ten seconds and that counts as a failure.
  • Six attempts with growing waits — a minute, five, fifteen, an hour, three and six — spread over about ten and a half hours. That covers a night deploy and a weekend maintenance window.
  • Thirty consecutive failures pause the endpoint, and the reason is recorded. You bring it back by setting it active again, which also resets the counter.
  • We only call public https addresses. It is checked when you register the endpoint and again before every delivery, because a name that resolves to the public internet today can resolve to your internal network tomorrow.
  • An endpoint registered today does not receive yesterday's events: a subscription starts counting when it is created, so building your first scenario does not fire forty runs at once.

Errors

Always the same shape

Every error returns the same object, with a code meant to be branched on. Codes are never renamed: they are added. The message beside it is for whoever reads the log, and that one can change.

{
  "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"
}

An unknown parameter is refused, not ignored. A filter that is silently dropped hands you more data than you asked for and nothing tells you.

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

CodeWhen it comes up
invalid_token401 · The key is missing, malformed or revoked. We do not say which of the three.
insufficient_scope403 · The key is valid but does not carry the scope that call needs. The reply says which one was needed and which ones it has.
module_required403 · The company has not enabled the corporate identity module, which is the one the API belongs to.
read_only403 · The free trial ended and the company is read-only. It only affects what writes.
rate_limited429 · More requests than allowed in a minute. The reply says how many seconds to wait.
invalid_request400 · A field is missing, unexpected or not understood. The message names the exact parameter and lists the values it takes.
not_found404 · Nothing of yours with that id. It is also what you get for something belonging to another company: telling the two apart would be a way of finding out what exists elsewhere.
conflict409 · It clashes with something that already exists: two people with the same email, two identical endpoint URLs. The reply carries the id of what it clashed with, so you can carry on.
server_error500 · Something broke on our side. None of it reaches the reply; the whole thing stays in our log.

Limits

What to know before writing the loop

  • One hundred and twenty requests per minute per key. Every reply carries how many are left and in how many seconds the window resets; a 429 also carries how long to wait.
  • Lists come fifty at a time and take up to two hundred per page. The first page is 1, not 0. Asking for more than two hundred is clamped rather than refused.
  • The metrics period goes up to three hundred and sixty-five days and is set by parameter. Reach by announcement is the expensive block: leave it out if your dashboard only draws the headline figures.
  • Twenty outgoing endpoints per company. It is not a commercial limit: every event is queued once per endpoint, and anyone who needs more is fanning out in the wrong place.
  • Outgoing events take up to a minute: they are detected by reading the database and not from the publish button, so one integration's slow server cannot hold up anybody's wall.
  • Only the last twenty-four hours are announced. If our clock task were stopped for a whole day, we do not release the quarter at you in one go: your automations would run on the day before yesterday's data as if it were current.

What it does not do

Said here so you do not find out halfway through

Whoever is about to integrate needs the limits before starting, not once half a scenario is built. This list is what a developer will look for and not find, and it is updated when something stops being true.

  • There is no screen for creating keys. Today they are issued from the console with npm run clave:api, and we issue them. An honest screen would need its text in the twelve languages of the application, and half a translated screen does not get published.
  • Outgoing events need a cron line on the server. Without it nothing breaks, and that is the problem: everything else keeps working and simply nothing goes out. You see it in the delivery log, which would be empty.
  • The signing secret is shown once: when the endpoint is created or when it is rotated. It cannot be read back, only rotated. Store it at that moment.
  • Rotating the secret has no grace period. The very next delivery is signed with the new one, so change it on your server first and ask for the rotation afterwards.
  • Twenty outgoing endpoints per company. If you need more, the fan-out has to happen in your own tool from a single delivery.
  • There is no access to chat, surveys, events, recognitions or albums. They show up in the announcement list and in outgoing events with their kind, but they cannot be created or read on their own.
  • There is no access to documents and no file upload. Nor images inside an announcement: the body is plain text.
  • An announcement that already exists cannot be edited or deleted, and comments can neither be written nor read.
  • People cannot be looked up by partial email search, and nobody can truly be deleted: deletion is a written request we handle, and it anonymises rather than removes.
  • Metrics are aggregated and there is no way to pull who read what. The only named list is the required-reading one, which exists so it can be shown to an inspection.
  • There is no separate sandbox, no official client libraries for any language and no cursor paging. It is JSON over HTTPS and you call it with whatever you have.

For machines

If an assistant is reading this

Half of a purchase evaluation today is somebody asking an assistant whether this connects to anything. So the answer is written to be readable without an account and without guessing.

  • The OpenAPI 3.1 specification sits at a public, stable URL, with no key, with cross-origin allowed and without touching the database. It describes the seventeen endpoints with their parameters, their accepted values, the shape of each reply and the five outgoing events.
  • It is not hand-written: it is generated from the same catalogue each endpoint validates against, and the response shapes are produced by running the very code that serves the requests. It cannot say anything different from what it does.
  • This page has the same structure in all six languages, with headings that say what is under them and real tables, not images and not fake columns.
  • There is also an llms.txt with the short version, with the integration section written out separately and with the URLs and scopes in plain text.

If something on that list is missing for you, say so

Whatever gets asked for most is what gets written next. And if anything on this page does not match what the server answers, that is a fault of ours and we want to hear about it the same day.