Developers
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.
OpenAPI 3.1 spec, no key needed
https://app.blastack.com/api/openapi.json
API root
https://app.blastack.com/api/v1
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
Eight facts. With these you can write the request without reading any further.
| What | Value | Note |
|---|---|---|
| API root | https://app.blastack.com/api/v1 | It lives in the application, not on this marketing site. One address, and it does not change. |
| Specification | https://app.blastack.com/api/openapi.json | Public and unauthenticated, so an importer can read it before you have any credentials. |
| Authentication | Authorization: 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. |
| Format | application/json; charset=utf-8 | JSON in, JSON out. There is no XML and no form encoding. |
| Rate limit | 120 req/min · RateLimit-Remaining | Per key, per minute. Every reply says how many are left and when the window resets. |
| Paging | page=1 · per_page=50 · ≤ 200 | The 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
All three are running. What is missing is further down, in its own section and named out loud.
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.
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.
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
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.
| Tool | Reads the spec? | Recommended trigger |
|---|---|---|
| n8n | Yes, the whole thing | Webhook node + POST /webhooks |
| Make | Yes, in the custom apps editor | Custom webhook + POST /webhooks |
| Zapier | No | Catch Hook + POST /webhooks |
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
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.
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
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 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
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
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.
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.
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.
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.
| Scope | What it opens |
|---|---|
| people:read | Reading the workforce and the departments and sites an announcement can be aimed at. It is all a dashboard needs. |
| people:write | Creating, updating and deactivating people. Deactivating closes their sessions, so it does not come free with the read scope. |
| announcements:read | Reading 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:write | Posting to the wall. It respects the company's review queue, so a key cannot jump anyone's approval circuit. |
| metrics:read | The aggregated figures. Nobody is named here. |
| webhooks:read | Seeing the registered endpoints and the delivery log. It is what answers 'why is nothing arriving' without opening a ticket. |
| webhooks:write | Registering, 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
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.
| Method | Path | Scope | What it does |
|---|---|---|---|
| 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. |
| 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. |
| 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. |
| 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. |
| 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. |
| 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. |
| 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. |
| 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. |
| 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. |
| 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. |
| 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. |
| GET | /webhooks | webhooks:read | Your registered endpoints, each with its health: consecutive failures, what your server last answered and, if we paused it, why. |
| 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. |
| GET | /webhooks/{id} | webhooks:read | One endpoint, health included. |
| 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. |
| 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. |
| 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. |
Parameters
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.
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.
| Parameter | Type | Accepted values | Default | Example | What it does |
|---|---|---|---|---|---|
| page | integermin 1 | 1 | 2 | Page to return. The first page is 1, not 0. | |
| per_page | integermin 1 · max 200 · out of range is clamped, not refused | 50 | 100 | How many items per page. Anything above 200 is clamped to 200 rather than rejected: asking for more is a reasonable thing to want. | |
| sort | string | created_at · updated_at · name · email | created_at | Field to sort by. Defaults to created_at. | |
| order | string | asc · desc | asc | Sort direction. | |
| fields | string · several, comma-separated | external_id · email · name · job_title · department · center · role · status · language · created_at · updated_at | external_id,department,status | Comma-separated list of fields to return. `id` is always included. Use it to avoid pulling personal data you do not need. | |
| status | string · several, comma-separated | active · invited · disabled | active | Only people in these states. Several may be given. | |
| role | string · several, comma-separated | owner · admin · gestor_personas · responsable_local · informatico · editor · revisor · analista · employee | Only people with these roles. Several may be given. | ||
| department | string · several, comma-separated | Maintenance,Quality | Department name, or several separated by commas. Case-insensitive: your directory writing MANTENIMIENTO is not an error. | ||
| center | string · several, comma-separated | Site name, or several separated by commas. Case-insensitive. | |||
| no_department | boolean | Only 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. | |||
| language | string · several, comma-separated | es · ca · en · pt · fr · it · de · sv · nl · pl · ro · cs | Only people who picked this reading language. | ||
| string | Exact email address, case-insensitive. | ||||
| external_id | string | Exact external_id, as sent by your HR system over SCIM. | |||
| q | string | garcia | Free text over name, email and job title. Case-insensitive, matches anywhere in the value. | ||
| updated_since | ISO 8601 date | 2026-08-01T00:00:00Z | Only 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_since | ISO 8601 date | Only people added at or after this instant. | |||
| created_until | ISO 8601 date | Only people added before this instant. |
people:read
One person, looked up by our id or by yours. Useful to refresh a record without walking the whole list.
| Parameter | Type | Accepted values | Default | Example | What it does |
|---|---|---|---|---|---|
| idRequired | string | a3f1… | 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. |
| Parameter | Type | Accepted values | Default | Example | What it does |
|---|---|---|---|---|---|
| fields | string · several, comma-separated | external_id · email · name · job_title · department · center · role · status · language · created_at · updated_at | external_id,department,status | Comma-separated list of fields to return. `id` is always included. Use it to avoid pulling personal data you do not need. |
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.
| Parameter | Type | Accepted values | Default | Example | What it does |
|---|---|---|---|---|---|
| nameRequired | stringmax length 120 | Ana García | Full name, as it should appear on the wall. | ||
| stringmax length 200 | Work 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_id | stringmax length 200 | EMP-0042 | The id this person has in your system. Store it and you can cross both lists later without matching names. | ||
| job_title | stringmax length 120 | Job title. | |||
| department | stringmax length 80 | Department name. Created if it does not exist. Without one, this person only ever sees company-wide announcements. | |||
| center | stringmax length 80 | Site name. Created if it does not exist. | |||
| role | string | admin · gestor_personas · responsable_local · informatico · editor · revisor · analista · employee | employee | What 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. | |
| status | string | active · invited · disabled | active | `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. | |
| language | string | es · ca · en · pt · fr · it · de · sv · nl · pl · ro · cs | Reading language. Leave it out and they read in the company's, which is what you want unless you actually know. |
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.
| Parameter | Type | Accepted values | Default | Example | What it does |
|---|---|---|---|---|---|
| idRequired | string | a3f1… | 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. |
| Parameter | Type | Accepted values | Default | Example | What it does |
|---|---|---|---|---|---|
| name | stringmax length 120 | Full name. | |||
| stringmax length 200 | Work email. | ||||
| external_id | stringmax length 200 | The id this person has in your system. | |||
| job_title | stringmax length 120 | Job title. | |||
| department | stringmax length 80 | Department name. Created if it does not exist. | |||
| center | stringmax length 80 | Site name. Created if it does not exist. | |||
| role | string | admin · gestor_personas · responsable_local · informatico · editor · revisor · analista · employee | What they can do. `owner` cannot be given from the API. | ||
| status | string | active · invited · disabled | Their state. `disabled` also closes their sessions. | ||
| language | string | es · ca · en · pt · fr · it · de · sv · nl · pl · ro · cs | Reading language. |
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.
| Parameter | Type | Accepted values | Default | Example | What it does |
|---|---|---|---|---|---|
| idRequired | string | a3f1… | 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. |
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.
| Parameter | Type | Accepted values | Default | Example | What it does |
|---|---|---|---|---|---|
| type | string | all · departments · centers | all | Return only departments, only sites, or both. | |
| q | string | Free text over the name. Case-insensitive. | |||
| with_people | boolean | Only 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. | |||
| sort | string | name · people | name | Field to sort by. Defaults to name. | |
| order | string | asc · desc | asc | Sort direction. |
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.
| Parameter | Type | Accepted values | Default | Example | What it does |
|---|---|---|---|---|---|
| page | integermin 1 | 1 | 2 | Page to return. The first page is 1, not 0. | |
| per_page | integermin 1 · max 200 · out of range is clamped, not refused | 50 | 100 | How many items per page. Anything above 200 is clamped to 200 rather than rejected: asking for more is a reasonable thing to want. | |
| sort | string | published_at · created_at · updated_at · title | published_at | Field to sort by. Defaults to published_at. | |
| order | string | asc · desc | asc | Sort direction. | |
| fields | string · several, comma-separated | title · body · status · kind · language · audience · community · section · author · notify · comments_enabled · pinned · acknowledgements · reads · published_at · created_at · updated_at · url | external_id,department,status | Comma-separated list of fields to return. `id` is always included. Use it to avoid pulling personal data you do not need. | |
| status | string · several, comma-separated | draft · pending_review · published · rejected · scheduled · archived | published,scheduled | Only these states. Several may be given. `scheduled` means it is written and waiting for its hour. | |
| author | string | Who signs it: our id, their email or their external_id. Any of the three. | |||
| audience_type | string | all · departments · centers | Only announcements aimed this way. | ||
| department | string · several, comma-separated | Only announcements aimed at these departments, by name. Several may be given. | |||
| center | string · several, comma-separated | Only announcements aimed at these sites, by name. | |||
| community | string | Only announcements in this community, by id or by name. | |||
| requires_acknowledgement | boolean | Only required reading, or only the rest. This is the filter a compliance report is built on. | |||
| pinned | boolean | Only pinned announcements, or only the rest. | |||
| q | string | Free text over title and body. Case-insensitive. | |||
| published_since | ISO 8601 date | Published at or after this instant. | |||
| published_until | ISO 8601 date | Published before this instant. | |||
| created_since | ISO 8601 date | Written at or after this instant. | |||
| updated_since | ISO 8601 date | Changed at or after this instant. |
announcements:read
One announcement. It is the next step after the id returned by a post or carried by an outgoing event.
| Parameter | Type | Accepted values | Default | Example | What it does |
|---|---|---|---|---|---|
| idRequired | string | The announcement id. |
| Parameter | Type | Accepted values | Default | Example | What it does |
|---|---|---|---|---|---|
| fields | string · several, comma-separated | title · body · status · kind · language · audience · community · section · author · notify · comments_enabled · pinned · acknowledgements · reads · published_at · created_at · updated_at · url | external_id,department,status | Comma-separated list of fields to return. `id` is always included. Use it to avoid pulling personal data you do not need. |
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.
| Parameter | Type | Accepted values | Default | Example | What it does |
|---|---|---|---|---|---|
| bodyRequired | stringmax length 8000 | The text. Plain text; line breaks are kept. | |||
| authorRequired | string | EMP-0042 | Who signs it: our id, their email or their external_id. Must be an active person in this company. | ||
| title | stringmax length 160 | Headline. | |||
| audience | object | Who 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.type | string | all · departments · centers | all | Aim at everyone, at departments or at sites. | |
| audience.ids | array | ["Maintenance","Quality"] | Department or site names, or their ids. Required unless type is `all`. | ||
| community | string | Which 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. | |||
| section | string | Which section it belongs to, by id or by name. It must belong to the same community. | |||
| publish_at | ISO 8601 date | 2026-09-01T06:00:00Z | Publish 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_acknowledgement | boolean | false | Required 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. | ||
| notify | boolean | true | Whether 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_enabled | boolean | true | Whether people can comment on it. | ||
| pinned | boolean | false | Keep it at the top of the wall. | ||
| language | string | es · ca · en · pt · fr · it · de · sv · nl · pl · ro · cs | What 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. |
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.
| Parameter | Type | Accepted values | Default | Example | What it does |
|---|---|---|---|---|---|
| idRequired | string | The announcement id. |
| Parameter | Type | Accepted values | Default | Example | What it does |
|---|---|---|---|---|---|
| page | integermin 1 | 1 | 2 | Page to return. The first page is 1, not 0. | |
| per_page | integermin 1 · max 200 · out of range is clamped, not refused | 50 | 100 | How many items per page. Anything above 200 is clamped to 200 rather than rejected: asking for more is a reasonable thing to want. | |
| state | string | all · acknowledged · pending | all | Everyone, only those who confirmed, or only those still pending. | |
| department | string · several, comma-separated | Only people in these departments, by name. | |||
| center | string · several, comma-separated | Only people at these sites, by name. | |||
| sort | string | name · acknowledged_at | name | Field to sort by. Defaults to name. | |
| order | string | asc · desc | asc | Sort direction. |
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.
| Parameter | Type | Accepted values | Default | Example | What it does |
|---|---|---|---|---|---|
| period_days | integermin 1 · max 365 | 30 | 90 | How many days back the period covers. Applies to every figure with a period, including the reach lists. | |
| include | string · several, comma-separated | workforce · announcements · reach_by_announcement · reach_by_department | workforce,announcements | Which 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. |
webhooks:read
Your registered endpoints, each with its health: consecutive failures, what your server last answered and, if we paused it, why.
| Parameter | Type | Accepted values | Default | Example | What it does |
|---|---|---|---|---|---|
| page | integermin 1 | 1 | 2 | Page to return. The first page is 1, not 0. | |
| per_page | integermin 1 · max 200 · out of range is clamped, not refused | 50 | 100 | How many items per page. Anything above 200 is clamped to 200 rather than rejected: asking for more is a reasonable thing to want. | |
| event | string | announcement.published · announcement.pending_review · announcement.acknowledged · person.created · person.deactivated | Only endpoints subscribed to this event. | ||
| active | boolean | Only live endpoints, or only the switched-off and paused. |
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.
| Parameter | Type | Accepted values | Default | Example | What it does |
|---|---|---|---|---|---|
| urlRequired | stringmax length 500 | https://hooks.example.com/blastack | Where we POST. https only, no credentials, no fragment. | ||
| eventsRequired | array | announcement.published · announcement.pending_review · announcement.acknowledged · person.created · person.deactivated | ["announcement.published"] | Which events this endpoint wants. At least one. | |
| name | stringmax length 120 | Zapier · switchboard | What it is for, in your words. It is the only thing that lets you delete the right one six months from now. | ||
| secret | stringmax length 200 | Your own signing secret, if you would rather set it than have one generated. At least 32 characters. | |||
| active | boolean | true | Start delivering straight away. |
webhooks:read
One endpoint, health included.
| Parameter | Type | Accepted values | Default | Example | What it does |
|---|---|---|---|---|---|
| idRequired | string | The webhook endpoint 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.
| Parameter | Type | Accepted values | Default | Example | What it does |
|---|---|---|---|---|---|
| idRequired | string | The webhook endpoint id. |
| Parameter | Type | Accepted values | Default | Example | What it does |
|---|---|---|---|---|---|
| url | stringmax length 500 | Where we POST. https only. | |||
| events | array | announcement.published · announcement.pending_review · announcement.acknowledged · person.created · person.deactivated | Which events this endpoint wants. At least one. | ||
| name | stringmax length 120 | What it is for. | |||
| active | boolean | Switch delivery on or off. Turning it on clears a pause. | |||
| rotate_secret | boolean | Generate a new signing secret and return it once in this reply. |
webhooks:write
Removes it along with its history. To stop deliveries for a while without losing the history, switch it off instead.
| Parameter | Type | Accepted values | Default | Example | What it does |
|---|---|---|---|---|---|
| idRequired | string | The webhook endpoint id. |
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.
| Parameter | Type | Accepted values | Default | Example | What it does |
|---|---|---|---|---|---|
| idRequired | string | The webhook endpoint id. |
| Parameter | Type | Accepted values | Default | Example | What it does |
|---|---|---|---|---|---|
| page | integermin 1 | 1 | 2 | Page to return. The first page is 1, not 0. | |
| per_page | integermin 1 · max 200 · out of range is clamped, not refused | 50 | 100 | How many items per page. Anything above 200 is clamped to 200 rather than rejected: asking for more is a reasonable thing to want. | |
| status | string | pending · delivered · given_up | Only deliveries in this state. | ||
| event | string | announcement.published · announcement.pending_review · announcement.acknowledged · person.created · person.deactivated | Only deliveries of this event type. | ||
| since | ISO 8601 date | Only events that happened at or after this instant. |
Examples
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
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
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
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
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.
| Event | When it fires |
|---|---|
| announcement.published | Something 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_review | An announcement is waiting for approval. It only fires in companies that have the review queue switched on. |
| announcement.acknowledged | Someone confirmed a required reading. One event per person, carrying who and when. |
| person.created | Someone was added, no matter how: provisioning, a join code, single sign-on, the admin screen or this API. |
| person.deactivated | Someone was deactivated. Since nobody is ever deleted here, this is the offboarding signal. |
{
"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.
| Header | What for |
|---|---|
| blastack-event | What happened. It is the value you branch on. |
| blastack-event-id | The 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-attempt | Which attempt this is, starting at the first. |
| blastack-signature | The signature. It is the only thing that separates a delivery of ours from one invented by whoever finds your URL. |
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.
Errors
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
| Code | When it comes up |
|---|---|
| invalid_token | 401 · The key is missing, malformed or revoked. We do not say which of the three. |
| insufficient_scope | 403 · 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_required | 403 · The company has not enabled the corporate identity module, which is the one the API belongs to. |
| read_only | 403 · The free trial ended and the company is read-only. It only affects what writes. |
| rate_limited | 429 · More requests than allowed in a minute. The reply says how many seconds to wait. |
| invalid_request | 400 · A field is missing, unexpected or not understood. The message names the exact parameter and lists the values it takes. |
| not_found | 404 · 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. |
| conflict | 409 · 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_error | 500 · Something broke on our side. None of it reaches the reply; the whole thing stays in our log. |
Limits
What it does not do
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.
For machines
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.
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.