Framework answers from consenting companies
You run your sustainability assessment on BeCause: your own framework, perhaps in tiers — a Basic, an Advanced, a Professional. Companies answer it on the platform and consent to share their answers with you, whether you are a distribution channel, a hotel chain or a marketplace. Come reporting season you want last year's answers from every consenting company, across all tiers, in one job. This walkthrough is the complete loop — one search request, a poll, a set of page downloads — plus a webhook variant that removes the polling.
Before you start
- An API key — Getting started covers creating one, Authentication what it acts as.
- Your framework ids, from
GET /api/v1/frameworks. Each entry is an id and a title; the id is what the search below means byframeworkIds. - Your distribution-channel id, shared by BeCause during onboarding. A hotel chain fetching its own properties' answers uses its group id instead —
GET /api/v1/groupslists them. - The ids of the custom properties you want returned per company — above all your own identifier for it, if BeCause holds one for you. The v2 search returns no custom properties unless you list their ids in
customPropertyIds, and that identifier is usually the only way to join the export back to your own system, so decide this before the first request, not after.GET /api/v1/custom-propertieslists your properties; the identifier-type ones carryisIdentifier: true.
1. Submit the search
Framework-answer search is a bulk endpoint: the request is queued, not answered while you wait — see Asynchronous processing. One request covers all your frameworks; three tiers do not need three jobs.
curl -X POST https://api.because.eco/api/v2/bulk/framework-answers/search \
-H "Authorization: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"searchBy": {
"distributionChannelId": "7d5a3f18-2b64-4c1e-9a0f-8e2d4c6b1a33"
},
"frameworkIds": [
"8f21b3c6-4e5d-4a2b-9c7e-1b3a5d7f0001",
"8f21b3c6-4e5d-4a2b-9c7e-1b3a5d7f0002",
"8f21b3c6-4e5d-4a2b-9c7e-1b3a5d7f0003"
],
"period": {
"years": [2025],
"reportingPeriodTypes": ["Yearly"]
},
"customPropertyIds": [
"0992e704-5c3b-4b6e-8a1d-7f2e9c4b6d18"
]
}'
What each part does:
searchBy— one ofdistributionChannelIdorgroupId. The channel form returns every company connected to your channel that has consented to share its data with you; a company that connects or consents later appears on the next run. The group form returns the group's members, subgroups included.frameworkIds— up to ten per request, no duplicates. Answers are reported per framework, so querying the tiers together costs nothing in clarity and saves two jobs.period.years— up to ten. Years select reporting periods by their start year — the value the result emits asstartYear. Omittingperiod, oryearsinside it, defaults to the current year; omission is never "all years".period.reportingPeriodTypes— which granularities to keep.["Yearly"]fits an annual pull, and it still includes the computed yearly totals BeCause derives for companies that reported monthly figures without entering a yearly one, so monthly-only reporters are not lost. Omit it to also receive the monthly answers as separate buckets.customPropertyIds— which custom properties to return under each company'sfields. Omitted means none.pageSize— optional, 100 to 10,000 companies per result page, default 5,000. The result is always paged; a result smaller than one page is simply one page.
Instead of fixed years, "period": { "latest": true } selects the single most recent reporting period per company and framework, searched within the current and two prior calendar years — the right shape for a "where does everyone stand now" pull. It cannot be combined with years or months.
The response is the handle for everything that follows:
{
"correlationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
2. Poll the task
curl https://api.because.eco/api/v2/bulk/tasks/3fa85f64-5717-4562-b3fc-2c963f66afa6 \
-H "Authorization: YOUR_API_KEY"
The status advances through Pending, Scheduled and InProgress; poll on an interval — seconds, not milliseconds — until one of the four terminal states:
Success— done; the result is ready.PartialSuccess— done, but some items failed along the way. The result is still there; read it.Error— the request failed;error.generalErrorMessagessays why.Cancelled— the request was cancelled before finishing.
On success, the response carries the result as a list of pages:
{
"correlationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"createdAtUtc": "2026-02-02T07:42:25.045Z",
"updatedAtUtc": "2026-02-02T07:43:12.981Z",
"status": "Success",
"requestType": "BulkGetFrameworkAnswers",
"result": {
"pageSize": 5000,
"pageCount": 2,
"pages": [
{
"url": "https://exports.because.eco/…/page-0001.json?sv=…",
"companyCount": 5000
},
{
"url": "https://exports.because.eco/…/page-0002.json?sv=…",
"companyCount": 1321
}
]
},
"error": null
}
pages is in page order and the last page is usually short; companyCount says how many companies each file holds. The same task endpoint reports every bulk request your key submits — a v1 search or a company mapping resolves there too, as a single unpaged entry rather than pages.
3. Download the pages
Each url is a self-contained link — GET it directly, no Authorization header — valid for thirty days. The file is gzip-compressed JSON served as application/json with Content-Encoding: gzip, so most HTTP clients — Postman, a browser, Python requests, axios — decompress it for you and hand you plain JSON. A client that does not honour Content-Encoding (bare curl is the common case) receives the gzip bytes unchanged: decompress them yourself, whether that is gunzip on the command line, a GZipStream in .NET, or zlib anywhere else.
curl --compressed "https://exports.because.eco/…/page-0001.json?sv=…" > page-1.json
Pages exported before early August 2026 are served the old way — application/gzip, no Content-Encoding header, a .json.gz name — and must be gunzipped by hand; those links age out within thirty days.
Every page is a complete document on its own — the catalogs described below are repeated in each — so pages can be processed independently and in parallel; nothing requires reading page one before page two.
What a page looks like
Datapoint and custom-property descriptions are declared once at the top; everything under companies references them by id. A field with no value is absent, not null. This is the shape in brief — the framework answers page reference lists every field.
{
"dataPoints": [
{
"id": "f12ed4cd-9a3b-4e7c-8d21-5b6a7c8d9e01",
"questionId": "2cc6d60c-1f4a-4b8e-9c3d-7e8f9a0b1c02",
"inFrameworks": [
"8f21b3c6-4e5d-4a2b-9c7e-1b3a5d7f0001",
"8f21b3c6-4e5d-4a2b-9c7e-1b3a5d7f0002"
]
},
{
"id": "7294d68d-3c5e-4f9a-b1d2-8e9f0a1b2c03",
"questionId": "9a41c7be-6d2f-4a8b-9e0c-1d2e3f4a5b04",
"inFrameworks": ["8f21b3c6-4e5d-4a2b-9c7e-1b3a5d7f0002"]
},
{
"id": "5069e92e-8b1c-4d6e-a3f4-9c0d1e2f3a05",
"questionId": "d1f82a55-4e7b-4c9d-8a1e-2f3a4b5c6d06",
"inFrameworks": ["8f21b3c6-4e5d-4a2b-9c7e-1b3a5d7f0002"],
"listOptions": [
{ "id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c07", "title": "Solar" },
{ "id": "e5f6a7b8-9c0d-4e1f-a2b3-4c5d6e7f8a08", "title": "Wind" }
]
}
],
"customProperties": [
{ "id": "0992e704-5c3b-4b6e-8a1d-7f2e9c4b6d18", "name": "Property code" }
],
"companies": [
{
"id": "9c56d3a1-8b0f-4c7a-9e2d-5b1a7f3e0c11",
"name": "Hotel Aurora",
"fields": {
"0992e704-5c3b-4b6e-8a1d-7f2e9c4b6d18": ["HA-2041"]
},
"frameworks": [
{
"frameworkId": "8f21b3c6-4e5d-4a2b-9c7e-1b3a5d7f0001",
"startYear": 2025,
"startMonth": "January",
"isComplete": true,
"progress": {
"totalQuestions": 11,
"totalRequiredQuestions": 11,
"addressedQuestions": 11,
"addressedRequiredQuestions": 11
},
"periods": [
{
"periodType": "Yearly",
"year": 2025,
"answers": [
{
"dataPointId": "f12ed4cd-9a3b-4e7c-8d21-5b6a7c8d9e01",
"answer": { "boolean": true }
}
]
}
]
},
{
"frameworkId": "8f21b3c6-4e5d-4a2b-9c7e-1b3a5d7f0002",
"startYear": 2025,
"startMonth": "January",
"isComplete": false,
"progress": {
"totalQuestions": 39,
"totalRequiredQuestions": 30,
"addressedQuestions": 24,
"addressedRequiredQuestions": 21
},
"periods": [
{
"periodType": "Yearly",
"year": 2025,
"answers": [
{
"dataPointId": "f12ed4cd-9a3b-4e7c-8d21-5b6a7c8d9e01",
"answer": { "boolean": true }
},
{
"dataPointId": "7294d68d-3c5e-4f9a-b1d2-8e9f0a1b2c03",
"answer": { "number": 41250.5 },
"unitId": "b1c9e0a4-2d3e-4f5a-8b6c-7d8e9f0a1b12"
},
{
"dataPointId": "5069e92e-8b1c-4d6e-a3f4-9c0d1e2f3a05",
"answer": { "list": ["a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c07"] }
}
]
}
],
"omittedQuestionIds": ["3e8b5f2a-7c1d-4e9f-a0b1-2c3d4e5f6a13"]
}
]
}
]
}
How to read it:
- The catalogs.
dataPointsdescribes every datapoint that carries an answer anywhere in the page; answers point back withdataPointId.questionIdgroups datapoints under the question they belong to. For list datapoints, the option titles live once inlistOptions; a list answer carries option ids only. fieldsis keyed by custom-property id, with the names once incustomProperties. This is where your own identifier for the company arrives —"HA-2041"above — and only the ids the search requested appear.- Shared datapoints appear under each framework. Tiers overlap by design, and here
f12ed4cd-…belongs to both Basic and Advanced, so Hotel Aurora's answer to it is reported under both framework entries.inFrameworkson the datapoint says exactly which of your requested frameworks contain it — when aggregating across tiers, use it to count such an answer once. - One framework entry per company, framework and period start.
startYearandstartMonthname the reporting period;isCompletesays whether the company finished the framework for it, andprogressbreaks that down by question counts — useful when you only accept answers from completed assessments. - Answers. Exactly one value key sits inside
answer:text,number,boolean,dateIso, a date range (dateRangeFromDateIso/dateRangeToDateIso),list,tupleorfileUrl. Numeric answers usually carry aunitId; resolve it throughGET /api/v1/unit-types. - What is not there is meaningful.
hiddenQuestionIdsnames questions the framework's own rules hide for this company — its other answers make them inapplicable;omittedQuestionIdsnames questions the company declared it will not answer for the period. Neither carries answers inperiods, and like every optional list in the payload, each is absent when empty. - Monthly data. Because the search kept only
Yearly, each framework entry holds one yearly bucket. Without that filter, an entry can carry a yearly bucket and several monthly ones side by side — neither derived from the other, so a consumer reading only one kind is reading a subset. - Scored frameworks additionally report
scoreCategoriesonprogressand on individual answers. The frameworks above do not score, so the field never appears for them.
The company id is the same company id the rest of the API uses, so the export joins directly against a company search or your existing company mappings.
4. Skip the polling
Instead of polling in step 2, configure a webhook on the four terminal topics. When bulk.task.succeeded (or bulk.task.partially_succeeded) arrives, verify the signature, check data.requestType is BulkGetFrameworkAnswers and that data.correlationId is the id you submitted — the webhook fires for every bulk request the profile submits — and go straight to the task call, whose result.pages is now populated.
Keeping it fresh
Answers accumulate as companies work through your frameworks, so during a reporting season re-run the search on a schedule — weekly is plenty for most programmes; mind the rate limits. Page URLs stop working after thirty days, so download promptly rather than bookmarking them.