Export policy acceptances
https://forumulate.com/api /orgs/{orgId}/spaces/{spaceId}/policy-gate/acceptances/export The acceptance log as a file: format=csv (default) or format=json, with the same filters as the list, up to 50,000 rows. CSV cells that a spreadsheet would run as a formula are prefixed with a quote. Up to 20,000 rows (EXPORT_SYNC_MAX_ROWS) the file is streamed in this response; above that the route answers 202 with a job (kind: "export.csv") the worker builds, so a long export is never held behind the edge’s ~30 s cut. Poll GET /orgs/{orgId}/exports/{id}, then download links.result; the file is kept 7 days and only the user who asked can fetch it.
Requires token scope: read — see "Token scopes" under Authentication.
Parameters
orgIdstringpathrequiredslug. One you are not a member of is a 404.spaceIdstringpathrequiredformatstringquerysubjectqueryemailquerysubjectEmailHash.emailHashquerykeyqueryversionquerypresentationstringqueryfromquerytoqueryscrolledToEndstringqueryforumulate-versionstringheaderforumulate-version: 2027-03-01 on every request from a new integration; it is optional only so that integrations written before versioning keep working. Each version includes everything before it:Code samples
cURL
curl -X GET "https://forumulate.com/api/orgs/{orgId}/spaces/{spaceId}/policy-gate/acceptances/export" \
-H "Authorization: Bearer $FORUMULATE_API_KEY" \
-H "forumulate-version: <value>"TypeScript
Python
Go
PHP
Ruby
Java
Responses
format=csv (the default) with one row per acceptance — accepted_at, subject, subject_email_hash, key, version, content_sha256, presentation, display, scrolled_to_end, event, verification, source_app, locale, ip, user_agent, batch_id, id — or format=json as { acceptances, truncated }. Cells a spreadsheet would run as a formula are prefixed with a quote."string"
Content-Dispositionstringrequest.malformed). A value that was understood but is not acceptable is a 422 validation.failed.Example
{
"error": {
"code": "string",
"message": "string",
"details": {},
"docs": "string",
"reason": "string",
"slugs": [
"string"
],
"conflict": {
"etag": "string",
"ops": [
{}
]
}
},
"requestId": "string"
}Schema
errorobjectrequiredShow child parameters
codestringrequiredrbac.forbidden.messagestringrequireddetailsobjectdocsstringGET /space-types/{type}/presets. Present on errors over a controlled list, so the answer to "what am I allowed to send?" is in the error itself.reasonstringiat_too_old, iat_in_future, bad_signature, bad_issuer, bad_audience, expired and more. Branch on this, not on message.slugsstring[]space.slug_taken, page.slug_taken, collection.slug_taken, site.slug_taken or badge.slug_taken.conflictobjectPATCH …/entries/{entryId}/fields) whose If-Match was stale.Show child parameters
etagstringrequiredopsarrayrequiredIf-Match that touch a path your own operations touch. Empty: none did — retry the batch unchanged under etag. null: the history no longer reaches your tag — re-read the entry first.requestIdstringx-request-id and x-forumulate-request-id response headers. Quote it in a bug report: it is the only thing that finds your exact request in our logs. It is in the body because that is the part that survives — an SDK that throws the parsed body, a serverless log that keeps the body and drops the headers, a ticket with the JSON pasted in.auth.required). Send a Bearer token or the session cookie.Same as 400 · GET /orgs/{orgId}/spaces/{spaceId}/policy-gate/acceptances/export
rbac.forbidden, whether what is missing is the capability, the token scope, or a resource the token is pinned away from. The message names the capability and, for a scope refusal, the scopes that would grant it. A refusal because of the PLAN is a 402 with quota.exceeded, not this.Same as 400 · GET /orgs/{orgId}/spaces/{spaceId}/policy-gate/acceptances/export
*.not_found).Same as 400 · GET /orgs/{orgId}/spaces/{spaceId}/policy-gate/acceptances/export
Same as 400 · GET /orgs/{orgId}/spaces/{spaceId}/policy-gate/acceptances/export
Same as 400 · GET /orgs/{orgId}/spaces/{spaceId}/policy-gate/acceptances/export