Your engagement data, in your own tools
A read-only REST API over your surveys, runs, aggregated results, people and dashboard metrics. Bearer-token authentication, cursor pagination, and a versioning promise written down rather than implied. Included with the Enterprise plan — a custom annual contract, not a self-serve upgrade. Talk to us.
One header, one key
Create a key in Settings → API. The full key is shown once, at creation. We store a SHA-256 hash of it, so if it's lost the only path forward is to revoke it and mint a new one.
# Every request carries the key as a bearer token.
curl https://app.bloomder.io/api/v1/surveys \
-H "Authorization: Bearer blm_live_YOUR_KEY_HERE"
What a key can and cannot do
- Scoped to the organization that created it. There is no cross-organization key.
- Read-only. The API has no write endpoints at all, so a leaked key cannot alter, delete or send anything.
- Revocable at any time by the account owner, taking effect on the next request.
- Optionally set to expire after a number of days. Left blank, a key does not expire — a silent expiry would break a customer's nightly job months after they set it up.
Treat a key like a password
A key grants read access to your whole organization's engagement data. Keep it in a secret manager, never in client-side code or a public repository, and revoke it the moment you suspect exposure. Every creation and revocation is recorded in your audit log.
What this API will not return
The anonymity guarantee is a property of the platform, not of the dashboard, so it applies here identically. This is the shortest honest summary of what you cannot get, even with a valid key.
- No individual responses. There is no endpoint that returns a single person's answers, because the platform deletes the link between a response and a person at the moment the response is saved.
- No small-group results. Aggregates for a run with fewer than five distinct respondents come back as
suppressed: true, carrying only the participation counts — which are known from the send list, not from any answer. - No way to lower the threshold. The floor of five is fixed in the deployed platform and cannot be changed by an administrator, by a query parameter, or by us.
Segmentation by department still works, because a department is a group attribute recorded when the invitation is sent, not an identity. Read the longer explanation on the security page.
Cursors, not page numbers
List endpoints return a page of data and a next_cursor. Pass that cursor back to get the next page; null means you've reached the end. Cursors are stable under inserts, which offset-based paging is not — a survey created mid-scan cannot make you skip or repeat a row.
# First page: 50 surveys.
curl "https://app.bloomder.io/api/v1/surveys?limit=50" \
-H "Authorization: Bearer $BLOOMDER_KEY"
# Response
{
"data": [ { "id": "clx…", "name": "Q3 Pulse", "status": "ACTIVE", … } ],
"next_cursor": "clx8f2k…"
}
# Next page.
curl "https://app.bloomder.io/api/v1/surveys?limit=50&cursor=clx8f2k…" \
-H "Authorization: Bearer $BLOOMDER_KEY"
limit defaults to 25 and is capped at 100. A larger value is clamped, not rejected.
Branch on the code, not the message
Every error returns the same envelope. code is a stable string that is part of the contract; error is human-readable prose that may be reworded at any time. Write your integration against the code.
{
"error": "The public API is not included in this plan. It is part of the Enterprise plan — contact us to enable it.",
"code": "plan_required"
}
| Status | Code | What it means |
|---|---|---|
| 400 | invalid_request | A parameter was malformed. |
| 401 | invalid_api_key | The key is missing, unknown, revoked or expired. One code for all four, deliberately: the response can't be used to probe which keys once existed. |
| 402 | plan_required | The organization's plan does not include API access. |
| 404 | not_found | No such resource in this organization. An id belonging to another customer returns 404, never 403. |
| 429 | rate_limited | Rate limit exceeded. Honour the Retry-After header. |
| 500 | internal_error | Something failed on our side. The body carries a request_id — quote it in a support request and we can find the exact log line. |
120 requests per minute, per key
The budget is per key rather than per organization or per IP: your integration runs from wherever your infrastructure happens to be, and two keys in one account should not compete for the same allowance.
X-RateLimit-LimitandX-RateLimit-Windowcome back on every response, not only on a 429, so a client can pace itself before it gets refused.- On a 429,
Retry-Aftergives the seconds to wait. Respect it rather than retrying immediately. - Need more for a bulk backfill? Write to [email protected] and tell us the shape of the job.
What we may change, and what we won't
An API is a promise about the future, so here is the exact promise. Every response also carries X-Bloomder-Api-Version with the date of the deployed contract.
Additive changes ship into /v1 without notice
- New endpoints.
- New fields in an existing response. Parse tolerantly: an unknown field is not an error.
- New values in an enum-like field, where the field already documents that it may grow.
Breaking changes never ship into /v1
Removing a field, renaming one, changing its type, or changing what an existing value means — all of those create /api/v2 instead. When that happens, /v1 keeps serving for at least six months and starts returning a Sunset header with the date it stops. You will also hear it from us by email before the header appears.
The OpenAPI document is the specification
This page explains the API; openapi.json defines it, and needs no key to read. Point Postman, Insomnia, or your generator of choice at it.
Every endpoint, in one table
All paths are relative to https://app.bloomder.io/api/v1. There are no write endpoints.
| Endpoint | Returns | Parameters |
|---|---|---|
| GET/surveys | Your surveys, newest first, with question and run counts. | limit, cursor |
| GET/surveys/{id} | One survey with its questions, its audience (departments, categories, divisions) and its schedule. | — |
| GET/surveys/{id}/runs | The survey's runs with invitations sent, responses received and response rate. | limit, cursor |
| GET/surveys/{id}/results | Aggregated results for one run: per-question distributions and averages, NPS, wellness average, department breakdown. Suppressed below five respondents. | run_id (omit for the latest run) |
| GET/employees | People on your list with their department, category and division. No response data. | limit, cursor, include_inactive |
| GET/departments | Your departments. | — |
| GET/categories | Your employee categories, used for survey targeting. | — |
| GET/divisions | Your divisions. | — |
| GET/action-items | Action items with priority, status, owner and due date. | limit, cursor |
| GET/metrics/dashboard | The same aggregates the dashboard renders for a time window. | range (7, 28, 90, 180, 365, all, Q1–Q4), department_id |
| GET/openapi.json | The OpenAPI 3.1 document. No authentication required. | — |
A complete example
Pull the latest results for every active survey — the shape most BI refreshes take.
#!/usr/bin/env bash
# Requires: curl, jq. Set BLOOMDER_KEY in your environment.
set -euo pipefail
BASE="https://app.bloomder.io/api/v1"
AUTH=(-H "Authorization: Bearer $BLOOMDER_KEY")
curl -s "$BASE/surveys?limit=100" "${AUTH[@]}" \
| jq -r '.data[] | select(.status == "ACTIVE") | .id' \
| while read -r id; do
curl -s "$BASE/surveys/$id/results" "${AUTH[@]}" \
| jq '{survey: .survey_name, rate: .run.response_rate, suppressed, nps: .nps_score}'
done
Ready to wire it up?
API access is part of the Enterprise plan, a custom annual contract. Talk to us about what you're building and we'll set it up; once it's on, you create keys from your account settings.
Questions about a specific integration? Write to [email protected].