Automate with the API
Create, validate, and preview row-level security policies programmatically
Everything the Security page does is available through the Management API, so you can provision policies as customers are onboarded, keep them in version control, or let an agent manage them.
Who can call these endpoints
Management endpoints require an organization Admin credential scoped to the same project. Dashboard tokens issued for your customers are never accepted for policy management.
The workflow
Whether a human or an agent is driving, the sequence is the same:
build the candidate
→ validate it
→ read the diagnostics
→ write only if it is valid
→ preview real viewersValidation is the step worth adopting early. Every create and update endpoint has a matching validation endpoint that runs exactly the same checks and saves nothing, so you can find out whether a change is acceptable before you make it.
Branch on codes, never on prose
Every problem Semaphor reports carries a stable code, the resource and field it applies to, and the repairs that would resolve it. Read those; do not pattern-match the human sentences, which are free to improve over time.
Each diagnostic gives you three pieces of copy for three different jobs:
titlesays what happenedmessageexplains whyremediations[].labelsays how to fix it
Use the codes to decide what to do, and show the copy to whoever is watching. When two repairs are both valid but mean different things for security, the diagnostic says so rather than picking one, and an agent should ask instead of guessing.
Validate before writing
Send a candidate to the matching validation endpoint. A well-formed request that
describes an unacceptable policy returns HTTP 200 with a status of INVALID.
That is a successful answer to the question you asked, not a transport failure,
so reserve your error handling for 4xx and 5xx.
POST /api/management/v1/projects/{projectId}/unified-security/definitions/validate
POST /api/management/v1/projects/{projectId}/unified-security/definitions/{definitionId}/validate
POST /api/management/v1/projects/{projectId}/unified-security/assignments/validate
POST /api/management/v1/projects/{projectId}/unified-security/assignments/{assignmentId}/validateThe validation endpoints take the same candidate body as the create or update
endpoint they mirror. The one exception is recovery: the definition update
endpoint also accepts a recoveryAction field for re-authoring a policy saved in
an older format, and the validation endpoint rejects it as an unknown field.
Before changing the all-tenants baseline
When an assignment change touches the all-tenants baseline, the response includes an impact summary: which parameters changed, and how many tenants and tenant users carry their own value for each. Those keep their own values; everyone else moves to the new baseline. Removing the baseline also reports that actors without their own assignment stop receiving the policy.
The summary counts assignments, not people, and does not enumerate every viewer.
Concurrency
Writes are last-valid-write-wins. A successful validation is not a lock: each write reloads the current saved state and validates against it. If two changes race, the later valid one wins.
Preview a viewer
POST /api/management/v1/projects/{projectId}/unified-security/previewAsk what a saved viewer receives today:
{
"connectionId": "conn_123",
"actor": { "kind": "TENANT", "tenantId": "tenant_ridgeline" }
}Or overlay one unsaved change and ask what it would do:
{
"connectionId": "conn_123",
"actor": { "kind": "TENANT", "tenantId": "tenant_ridgeline" },
"draft": {
"kind": "ASSIGNMENT_UPDATE",
"assignmentId": "usa_123",
"candidate": {
"params": { "allowed_locations": ["chicago"] }
}
}
}A request carries at most one draft, and Semaphor applies it once, in memory. The
draft kinds are DEFINITION_CREATE, DEFINITION_UPDATE, ASSIGNMENT_CREATE, and
ASSIGNMENT_UPDATE. You cannot combine ignorePersistedAssignments with a draft
or with assignmentId.
Reading the response
A blocked policy is still a successful request:
{
"ok": true,
"data": {
"preview": {
"status": "BLOCKED",
"diagnostics": [
{
"code": "PARAM_UNRESOLVED",
"severity": "ERROR",
"title": "Required RLS parameter has no value",
"message": "Set the assignment value.",
"retryable": false
}
],
"activation": {
"status": "ACTIVE",
"definitionCount": 1,
"ruleCount": 1
},
"compilation": { "status": "NOT_RUN" }
},
"context": {
"cls": { "filePathTemplates": {}, "params": {} },
"sls": {},
"sources": { "cls": [], "sls": [] }
}
}
}Work through it in this order:
- confirm
ok - read
preview.status - read
activation.statusto tell "blocked" apart from "this viewer has no policy" - group
diagnosticsbycodeand field path - follow only the remediations the response supplied
- revalidate and preview again after each repair
compilation reports whether the rules were checked against real tables.
CONTEXT_REQUIRED means the policy resolved but you did not say which tables the
query touches, so a broad matcher could not be verified. It is not a configuration
problem. Send referencedEntities when you want that confirmation; a policy whose
rules all use TABLE_LIST matchers is verified without it.
If the signed input also carried row filters of its own, they appear separately
under context.tokenOnlyRLS, and a legacy-mode connection reports its existing
rules under context.legacyRLS. Neither changes the saved policy's result.
HTTP status reference
| Status | What it means | What to do |
|---|---|---|
200 | The request succeeded | Read the typed result; it may still be blocked or invalid |
201 | A definition or assignment was created | Read the saved resource from the response |
400 | The request was malformed | Fix the reported field paths |
401 / 403 | Not authorized | Obtain an organization Admin credential for this project |
404 | A resource you named is gone | Reload; a definition or assignment may be stale |
409 | The write conflicts with saved state | Reload and resolve the reported conflict |
422 | The policy is invalid; nothing was saved | Repair the diagnostics and retry |
500 | Unexpected failure | Retry is safe; keep the requestId for support |
Field-level problems come back with an RFC 6901 pointer such as
/rlsConfig/parameters/allowed_locations, so you can attach a message to the right
input without parsing text.
Keep sensitive values out of your logs
Preview returns bounded summaries rather than raw data: arrays report their type and length, long strings are truncated, and secret-backed values stay masked. Keep that property in whatever you build on top. Do not log raw token parameters, secret values, complete customer lists, or internal identifiers that the response did not already expose.