Rotate the policy gate signing key
https://forumulate.com/api /orgs/{orgId}/spaces/{spaceId}/policy-gate/keys/rotate Mints a new signing key and returns its secret ONCE (only an encrypted copy is kept). The previous key keeps verifying tickets and tokens for overlapHours (default 24, 0–168), so you can deploy the new secret without a single failed verification. Configure your app with both kid:secret pairs during the overlap. Needs an admin-scope token: a secret that signs tickets can accept in any user's name.
Requires token scope: admin — see "Token scopes" under Authentication.
Parameters
orgIdstringpathrequiredslug. One you are not a member of is a 404.spaceIdstringpathrequiredforumulate-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:Idempotency-Keystringheader409. The key is scoped to the method and path, so the same key on a different endpoint is a different request. Error responses of 5xx are never cached — those retries re-run. A key is bound to the BODY it was first used with: reusing it with a different body is a 409 idempotency.key_reused, not a replay, because replaying would tell you a write landed that never ran. Generate one key per request, never per process or per session.dryRunbooleanquerytrue: authenticate, authorize and validate this request (body, query and path), then answer 200 {dryRun: true, ok: true, operation, checks} WITHOUT carrying it out — nothing is written, queued, mailed or announced. It does not compute a diff, and a write that passes may still be refused by a state check at write time (a slug taken in between). Anything other than true/false is a 422.Request body
application/json
overlapHoursinteger{
"overlapHours": 0
}Code samples
cURL
curl -X POST "https://forumulate.com/api/orgs/{orgId}/spaces/{spaceId}/policy-gate/keys/rotate" \
-H "Authorization: Bearer $FORUMULATE_API_KEY" \
-H "forumulate-version: <value>" \
-H "Idempotency-Key: <value>" \
-H "Content-Type: application/json" \
-d '{"overlapHours":0}'TypeScript
Python
Go
PHP
Ruby
Java
Responses
data mirror and nothing to GET back. An acknowledgement, a computed answer, a tally or a one-time credential — read the keys this response documents.Example
{
"keys": [
{
"active": true,
"createdAt": "string",
"kid": "string",
"primary": true,
"retiresAt": "string",
"revokedAt": "string"
}
],
"kid": "string",
"secret": "string",
"warnings": [
"tags: \"Launch Day\" is stored as \"launch-day\""
]
}Schema
keysPolicyGateKeySummary[]requiredShow child parameters
activebooleanrequiredcreatedAtstringrequiredkidstringrequiredprimarybooleanrequiredretiresAtstringrequiredrevokedAtstringrequiredkidstringrequiredsecretstringrequiredwarningsstring[][]. The same sentences are on the X-Field-Warnings response header. If this is present, something you sent is not what is stored; read the resource back rather than assuming your own value.request.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 · POST /orgs/{orgId}/spaces/{spaceId}/policy-gate/keys/rotate
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 · POST /orgs/{orgId}/spaces/{spaceId}/policy-gate/keys/rotate
*.not_found).Same as 400 · POST /orgs/{orgId}/spaces/{spaceId}/policy-gate/keys/rotate
validation.failed). error.details names the field paths.Same as 400 · POST /orgs/{orgId}/spaces/{spaceId}/policy-gate/keys/rotate
Same as 400 · POST /orgs/{orgId}/spaces/{spaceId}/policy-gate/keys/rotate
Same as 400 · POST /orgs/{orgId}/spaces/{spaceId}/policy-gate/keys/rotate