Skip to Content
API ReferenceGovernance

Governance API

The Governance module provides fine-grained access control and compliance features including role-based access control (RBAC), organization groups, destination policies, access policies, folder and destination access lists, and deletion rules.

Access to a workspace is granted only through a group: a group lives on an organization, holds people, and is granted roles on the workspaces it should reach. There is no direct workspace membership and no per-group permission grant.

Endpoint Groups

GroupDescription
RolesSystem and custom role management
PermissionsGranular permission definitions
GroupsOrganization-scoped group management
GrantsWhich roles a group holds on which workspace
Group MembersWho is in a group
Destination PoliciesSync-time governance policies
Access PoliciesData row-level access restrictions
Deletion RulesAudience-scoped warehouse deletion with audit logs

Roles

A role is a named set of permissions. It is not held by a person — it is held by a grant, which gives one group one role on one workspace. Zeotap includes three built-in roles (owner, admin, member) and supports custom roles.

Custom roles belong to the organization, not to a workspace, so a role is written once and can be granted on every workspace in it.

MethodPathDescription
GET/api/v1/organizations/{orgId}/rolesList the built-in roles plus this organization’s custom roles
POST/api/v1/organizations/{orgId}/rolesCreate a custom role
PUT/api/v1/organizations/{orgId}/roles/{roleId}Update a custom role
DELETE/api/v1/organizations/{orgId}/roles/{roleId}Delete a custom role
GET/api/v1/rolesList the built-in roles, with no organization context

Creating, updating and deleting a role requires roles.write, which in practice means membership of the organization’s administrators group.

List Roles

GET /api/v1/organizations/{orgId}/roles

[ { "id": "00000000-0000-0000-0000-000000000001", "organization_id": null, "name": "owner", "description": "Full access including workspace deletion", "is_system": true, "permissions": ["sources.read", "sources.write", "models.read", "..."], "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" }, { "id": "550e8400-e29b-41d4-a716-446655440000", "organization_id": "880e8400-e29b-41d4-a716-446655440000", "name": "data_analyst", "description": "Read-only access to models and audiences", "is_system": false, "permissions": ["sources.read", "models.read", "audiences.read"], "created_at": "2024-01-15T09:30:00Z", "updated_at": "2024-01-15T09:30:00Z" } ]

Create Custom Role

POST /api/v1/organizations/{orgId}/roles

Request Body

FieldTypeRequiredDescription
namestringYesRole name, unique within the organization. owner, admin and member are reserved
descriptionstringNoDescription of the role’s purpose
permissionsarray of stringsYesPermission keys to grant. Each is matched against the catalog exactly, so list the individual keys a category expands to
{ "name": "data_analyst", "description": "Read-only access to models and audiences", "permissions": [ "sources.read", "models.read", "audiences.read", "traits.read" ] }

System Roles

RoleIDDescriptionPermissions
owner00000000-0000-0000-0000-000000000001Full access including workspace deletion119 of 120
admin00000000-0000-0000-0000-000000000002Full access except workspace deletion118 of 120
member00000000-0000-0000-0000-000000000003Read-only access49 of 120

System roles carry a null organization_id and is_system: true, and cannot be edited or deleted.

Deleting a custom role does not fail when grants still hold it. Every grant carrying the role is moved to the built-in member role first, so those groups keep reaching their workspaces with reduced permissions rather than losing access outright. Check who holds the role before deleting it.


Permissions

Permissions are granular access control strings organized by category.

MethodPathDescription
GET/api/v1/permissionsList all available permissions (global catalog)
GET/api/v1/workspaces/{id}/permissions/meGet the caller’s permissions in this workspace

Response

[ { "id": "550e8400-e29b-41d4-a716-446655440000", "key": "sources.read", "category": "sources", "description": "View sources and connection details", "created_at": "2024-01-01T00:00:00Z" }, { "id": "660e8400-e29b-41d4-a716-446655440000", "key": "sources.write", "category": "sources", "description": "Create and update sources", "created_at": "2024-01-01T00:00:00Z" } ]

Permission Keys

Keys are {category}.{action}. Most categories pair read with write, where write covers creating, editing and deleting. Where one action has to be grantable on its own it gets its own key — syncs.trigger, stores.trigger, deletion_rules.execute, warehouse_users.rotate, workspace_audit.export, events.debug. A category with sub-resources nests them before the action, as events.contracts.read does.

The catalog holds 120 permissions across 43 categories. GET /api/v1/permissions returns every row, which is the authoritative list; the permissions reference is the same catalog with the role matrix beside it.


Groups

A group lives on an organization, holds people, and is granted roles on the workspaces it should reach. Groups are the only way to grant access to a workspace — there is no direct workspace membership.

MethodPathDescription
GET/api/v1/organizations/{orgId}/groupsList the organization’s groups
POST/api/v1/organizations/{orgId}/groupsCreate a group
GET/api/v1/organizations/{orgId}/groups/{groupId}Get a group, with its grants
PUT/api/v1/organizations/{orgId}/groups/{groupId}Rename a group
DELETE/api/v1/organizations/{orgId}/groups/{groupId}Delete a group
GET/api/v1/workspaces/{id}/groupsList the grants reaching this workspace — the read behind the workspace Groups tab

Create Group

POST /api/v1/organizations/{orgId}/groups

{ "name": "Marketing Team", "description": "Marketing department members" }

A group carries no role of its own. Roles are attached per workspace, through a grant — see Grants below.

Group Object

FieldTypeDescription
idstring (UUID)Unique identifier
organization_idstring (UUID)Owning organization
namestringGroup name, unique within the organization (case-insensitive)
descriptionstringDescription
system_keystringorg_admins or org_viewers for the two groups every organization is seeded with, absent for every other group. System groups cannot be renamed or deleted
member_countintegerNumber of people in the group
workspace_countintegerHow many distinct workspaces the group reaches. A group holding two roles on one workspace reaches one workspace
org_widebooleantrue when the group holds an organization-wide grant — every workspace in the organization, including ones created later
assignmentsarray of grantsReturned by the detail paths, not by list
created_atstring (ISO 8601)Creation timestamp
updated_atstring (ISO 8601)Last update timestamp

Grants

A grant is one group, one workspace, and the roles it holds there. Several roles are allowed, and the permissions they carry are unioned.

MethodPathDescription
PUT/api/v1/organizations/{orgId}/groups/{groupId}/workspaces/{workspaceId}Set the roles this group holds on this workspace
DELETE/api/v1/organizations/{orgId}/groups/{groupId}/workspaces/{workspaceId}Revoke the group’s access to this workspace

Set Workspace Roles

PUT /api/v1/organizations/{orgId}/groups/{groupId}/workspaces/{workspaceId}

{ "role_ids": [ "00000000-0000-0000-0000-000000000003", "550e8400-e29b-41d4-a716-446655440000" ] }

The body states the complete set of roles the group should hold on that workspace — roles you leave out are removed. An empty or absent role_ids attaches the group to the workspace granting nothing, which is how a group carries an access policy without carrying a role. The single-role spelling {"role_id": "..."} is still accepted; role_ids wins when both are sent.

Grant Object

FieldTypeDescription
idstring (UUID)Unique identifier
group_idstring (UUID)The group holding the grant
group_namestringJoined for display
organization_idstring (UUID)The organization both sides belong to
workspace_idstring (UUID) or absentThe workspace reached. Absent means every workspace in the organization, now and in future — the organization-wide grant the system groups hold
workspace_namestringJoined for display
role_idstring (UUID) or absentThe role granted. Absent means attached, granting nothing
role_namestringJoined for display
created_atstring (ISO 8601)Creation timestamp
updated_atstring (ISO 8601)Last update timestamp

One grant row carries one role, so a group holding two roles on a workspace has two rows.


Group Members

Manage who is in a group. Adding somebody to a group is what gives them access to every workspace that group reaches.

MethodPathDescription
GET/api/v1/organizations/{orgId}/groups/{groupId}/membersList group members
POST/api/v1/organizations/{orgId}/groups/{groupId}/membersAdd a member
DELETE/api/v1/organizations/{orgId}/groups/{groupId}/members/{accountId}Remove a member
GET/api/v1/workspaces/{id}/membersList everyone who can reach this workspace, with the groups and roles that let them (derived, read-only)

Add Member

POST /api/v1/organizations/{orgId}/groups/{groupId}/members

{ "account_id": "770e8400-e29b-41d4-a716-446655440000" }

Removing the last member of the Organization admins group is refused — it would leave the organization with nobody able to administer it. Add the replacement administrator first.

Group Member Object

FieldTypeDescription
group_idstring (UUID)Group ID
account_idstring (UUID)Account ID
emailstringMember email
namestringMember name
avatar_urlstringMember avatar URL
created_atstring (ISO 8601)When the member was added

Destination Policies

Destination policies are sync-time governance policies that restrict which records can be sent to specific destination types. They act as guardrails to prevent sensitive data from reaching certain platforms. The API path remains destination-rules for backwards compatibility.

MethodPathDescription
GET/api/v1/workspaces/{id}/destination-rulesList all destination policies
POST/api/v1/workspaces/{id}/destination-rulesCreate a destination policy
GET/api/v1/workspaces/{id}/destination-rules/{ruleId}Get a destination policy
PUT/api/v1/workspaces/{id}/destination-rules/{ruleId}Update a destination policy
DELETE/api/v1/workspaces/{id}/destination-rules/{ruleId}Delete a destination policy
GET/api/v1/workspaces/{id}/destination-types/{destType}/rulesList destination policies by destination type
GET/api/v1/workspaces/{id}/destination-rules/{ruleId}/groupsList the groups that may manage this policy
PUT/api/v1/workspaces/{id}/destination-rules/{ruleId}/groupsSet the groups that may manage this policy

Create Destination Policy

POST /api/v1/workspaces/{id}/destination-rules

{ "name": "GDPR - No EU Customers to Facebook", "description": "Prevent EU customer data from being synced to Facebook Ads", "parent_model_id": "770e8400-e29b-41d4-a716-446655440000", "destination_type": "facebook_ads", "filter_tree": { "type": "condition", "condition_type": "property", "column": "country", "operator": "not_in", "value": ["DE", "FR", "IT", "ES", "NL", "BE", "AT", "SE", "DK", "FI"] }, "enabled": true }

Destination Policy Object

FieldTypeDescription
idstring (UUID)Unique identifier
workspace_idstring (UUID)Owning workspace
parent_model_idstring (UUID)Model the policy applies to
destination_typestringDestination type this policy restricts
namestringDisplay name
descriptionstringDescription
filter_treeobjectFilter criteria (same format as audience filters)
enabledbooleanWhether the policy is active
created_bystring (UUID)Account that created the policy
created_atstring (ISO 8601)Creation timestamp
updated_atstring (ISO 8601)Last update timestamp

Who May Manage a Policy

A destination policy can name the groups allowed to edit and delete it. This governs editing the policy, not who the policy applies to — a policy is evaluated at sync time, where no user is present, so it always applies to every sync of its destination type.

PUT /api/v1/workspaces/{id}/destination-rules/{ruleId}/groups

{ "group_ids": ["990e8400-e29b-41d4-a716-446655440000"] }

The body states the complete set, so sending {"group_ids": []} clears every claim.

  • A policy naming no group can be managed by anyone with destination_rules.write. Absence is permissive, so nothing changes for policies that predate this.
  • Once a policy names groups, only destination_rules.write holders who belong to one of them may change it — plus workspace owners and admins, so a policy cannot be stranded by deleting the group that claimed it.
  • Re-claiming is behind the same check, so a claimed policy cannot be taken over by rewriting its claims.

Access Policies

Access Policies provide row-level access control by restricting which records a user or group can see. They use filter trees (same as audiences) to define visibility boundaries. The API path remains subsets for backwards compatibility.

Access Policy Categories

Categories organize access policies into logical groups (e.g., Region, Brand, Business Unit).

MethodPathDescription
GET/api/v1/workspaces/{id}/subset-categoriesList categories
GET/api/v1/workspaces/{id}/subset-categories/{catId}Get a category
POST/api/v1/workspaces/{id}/subset-categoriesCreate a category
PUT/api/v1/workspaces/{id}/subset-categories/{catId}Update a category
DELETE/api/v1/workspaces/{id}/subset-categories/{catId}Delete a category

Access Policy Endpoints

MethodPathDescription
GET/api/v1/workspaces/{id}/subsetsList all access policies
GET/api/v1/workspaces/{id}/subsets/meList access policies that apply to the caller
POST/api/v1/workspaces/{id}/subsetsCreate an access policy
GET/api/v1/workspaces/{id}/subsets/{subsetId}Get an access policy
PUT/api/v1/workspaces/{id}/subsets/{subsetId}Update an access policy
DELETE/api/v1/workspaces/{id}/subsets/{subsetId}Delete an access policy

Create Access Policy

{ "name": "US Region", "description": "Only US customer records", "category_id": "550e8400-e29b-41d4-a716-446655440000", "parent_model_id": "770e8400-e29b-41d4-a716-446655440000", "filter_tree": { "type": "condition", "condition_type": "property", "column": "country", "operator": "equals", "value": "US" } }

Access Policy Assignments

Assign access policies to groups to restrict their data visibility. Assignments are nested under each access policy.

MethodPathDescription
GET/api/v1/workspaces/{id}/subsets/{subsetId}/assignmentsList assignments for an access policy
POST/api/v1/workspaces/{id}/subsets/{subsetId}/assignmentsCreate an assignment
DELETE/api/v1/workspaces/{id}/subsets/{subsetId}/assignments/{assignmentId}Remove an assignment
GET/api/v1/workspaces/{id}/groups/{groupId}/subsetsList access policies assigned to a group
PUT/api/v1/workspaces/{id}/groups/{groupId}/subsetsReplace the set of access policies assigned to a group

Create Assignment

{ "group_id": "aae8400-e29b-41d4-a716-446655440000" }

Access Policy Object

FieldTypeDescription
idstring (UUID)Unique identifier
category_idstring (UUID)Parent category
workspace_idstring (UUID)Owning workspace
parent_model_idstring (UUID) or nullModel the access policy applies to
namestringDisplay name
descriptionstringDescription
filter_treeobjectFilter criteria
created_bystring (UUID)Account that created the access policy
created_atstring (ISO 8601)Creation timestamp
updated_atstring (ISO 8601)Last update timestamp

Folder and Destination Access Lists

Restrict a folder or a destination to named groups. A resource with no groups is reachable by everyone holding the ordinary read permission. A restricted folder also restricts every folder below it and the audiences, orchestrations and A/B tests filed in them; a restricted destination also restricts the syncs that send to it. Someone with no access gets 404; someone with view-only access who tries to change something gets 403. Editing an access list requires the resource’s own write permission (folders.write or destinations.write). See Folder and Destination Access.

MethodPathDescription
GET/api/v1/workspaces/{id}/folders/{folderId}/groupsGet a folder’s access list
PUT/api/v1/workspaces/{id}/folders/{folderId}/groupsReplace a folder’s access list
GET/api/v1/workspaces/{id}/folder-groupsList every restricted folder in the workspace
GET/api/v1/workspaces/{id}/destinations/{destId}/groupsGet a destination’s access list
PUT/api/v1/workspaces/{id}/destinations/{destId}/groupsReplace a destination’s access list
GET/api/v1/workspaces/{id}/destination-groupsList every restricted destination in the workspace

Set Access List

A PUT states the complete list; an empty groups removes the restriction. Naming a group already on the list changes its level.

{ "groups": [ { "group_id": "aae8400-e29b-41d4-a716-446655440000", "access_level": "edit" }, { "group_id": "bbe8400-e29b-41d4-a716-446655440000", "access_level": "view" } ] }
FieldTypeDescription
groupsobject[]{ group_id, access_level } per group. access_level is edit (see and change; for a destination, also send to it) or view (see only). Each group must reach this workspace. Any other level is rejected with 400.
group_idsstring[]The older form: every group at edit. Ignored when groups is present.

Access List Object

FieldTypeDescription
groupsarray{ group_id, group_name, access_level } for each named group; empty when unrestricted

The workspace-wide reads return an array of { resource_id, name, groups }, one per restricted resource. They require the write permission and include resources restricted to groups the caller is not in, so a restriction can always be lifted.


Deletion Rules

Deletion rules erase warehouse data for an audience-scoped population, on demand or on a schedule, and record every run in an audit log. See Deletion Rules for the eligibility rules and safety controls.

MethodPathDescription
GET/api/v1/workspaces/{id}/deletion-rulesList all deletion rules
POST/api/v1/workspaces/{id}/deletion-rulesCreate a deletion rule
POST/api/v1/workspaces/{id}/deletion-rules/impactResolve what else reads a candidate target list
POST/api/v1/workspaces/{id}/deletion-rules/estimateCount the rows each candidate target would lose
GET/api/v1/workspaces/{id}/deletion-rules/{ruleId}Get a deletion rule
PUT/api/v1/workspaces/{id}/deletion-rules/{ruleId}Update a deletion rule
DELETE/api/v1/workspaces/{id}/deletion-rules/{ruleId}Delete a deletion rule (its log is retained)
POST/api/v1/workspaces/{id}/deletion-rules/{ruleId}/dry-runStart a non-destructive run
POST/api/v1/workspaces/{id}/deletion-rules/{ruleId}/runStart a live run
GET/api/v1/workspaces/{id}/deletion-rules/{ruleId}/runsList a rule’s runs
GET/api/v1/workspaces/{id}/deletion-runsList the workspace deletion log
GET/api/v1/workspaces/{id}/deletion-runs/{runId}Get one run with per-target detail

Reads require deletion_rules.read, writes deletion_rules.write, and both run endpoints deletion_rules.execute. The estimate endpoint requires deletion_rules.write: it compiles the population, writes a scratch table into the planner schema and reads every target table, so it is part of authoring a rule rather than reading one.

Every endpoint additionally requires the workspace to have the Deletion Rules feature enabled by a platform administrator; otherwise they answer 403 deletion rules are not enabled for this workspace. See Availability.

Eligibility and safety refusals — an ineligible target, an empty filter, a column that cannot be cleared, a pinned join that has since drifted — answer 400 with the specific reason in error. Read it: it names the target and says what is wrong with it, which is the only place that information exists.

Create Deletion Rule

POST /api/v1/workspaces/{id}/deletion-rules

filter_tree is required and must carry at least one condition — a rule may not match every record by default. dry_run defaults to true and enabled to false, so omitting them creates an observe-only rule.

{ "parent_model_id": "770e8400-e29b-41d4-a716-446655440000", "name": "Erase opted-out EU customers", "description": "Fulfils standing right-to-erasure requests for EU opt-outs.", "filter_tree": { "type": "and", "children": [ { "type": "condition", "condition_type": "property", "field": "region", "operator": "equals", "value": "EU" }, { "type": "condition", "condition_type": "property", "field": "erasure_requested", "operator": "equals", "value": true } ] }, "targets": [ { "model_id": "770e8400-e29b-41d4-a716-446655440000", "action": "delete_rows" }, { "model_id": "881e8400-e29b-41d4-a716-446655440000", "action": "null_columns", "columns": ["shipping_address", "phone"] } ], "schedule": "0 3 * * *", "dry_run": true, "enabled": false }

Estimate Rows Affected

POST /api/v1/workspaces/{id}/deletion-rules/estimate

Counts how many rows each target would delete or clear right now, for a candidate definition that need not be saved. Nothing is mutated.

The count is not an approximation: it materializes the matched population exactly as a run does and issues the same per-target COUNT the run records as records_affected, so what you preview is what a run reports. It is never cached — call it again to re-measure.

{ "parent_model_id": "770e8400-e29b-41d4-a716-446655440000", "filter_tree": { "type": "and", "children": [ /* … */ ] }, "targets": [ { "model_id": "770e8400-e29b-41d4-a716-446655440000", "action": "delete_rows" }, { "model_id": "881e8400-e29b-41d4-a716-446655440000", "action": "null_columns", "columns": ["shipping_address"] } ] }
{ "matched_count": 1482, "targets": [ { "model_id": "770e8400-e29b-41d4-a716-446655440000", "model_name": "Customers", "physical_table": "\"ANALYTICS\".\"PUBLIC\".\"CUSTOMERS\"", "action": "delete_rows", "records_affected": 1482 }, { "model_id": "881e8400-e29b-41d4-a716-446655440000", "model_name": "Orders", "physical_table": "\"ANALYTICS\".\"PUBLIC\".\"ORDERS\"", "action": "null_columns", "columns": ["SHIPPING_ADDRESS"], "records_affected": 5310 } ] }

matched_count is the population — the number the audience estimate reports for the same filter. Each target’s records_affected is the rows that model loses, which is a different number: one matched customer can own several orders.

A target whose count fails carries an error string and the others still report, mirroring how a run records a per-target failure and carries on. Treat such a target as unknown, not as zero.

A null_columns count only includes rows where at least one selected column still holds a value — the same predicate the UPDATE uses. Counting again after a live run therefore reports 0, which is the property that makes re-running a rule a no-op.

Deletion Rule Object

FieldTypeDescription
idstring (UUID)Unique identifier
workspace_idstring (UUID)Owning workspace
parent_model_idstring (UUID)Model defining the entity the rule erases
namestringDisplay name (unique per workspace)
descriptionstringDescription
filter_treeobjectMatch criteria (same format as audience filters)
targetsarrayTarget selections — see below
schedulestringCron expression, optionally CRON_TZ=-prefixed. Empty means manual only
enabledbooleanRequired for any live deletion
dry_runbooleanObserve mode: every run reports and mutates nothing
last_run_atstring (ISO 8601) or nullWhen the most recent run started
last_run_statusstringStatus of the most recent run
created_bystring (UUID)Account that created the rule
created_atstring (ISO 8601)Creation timestamp
updated_atstring (ISO 8601)Last update timestamp

Deletion Target Object

FieldTypeDescription
model_idstring (UUID)Target model. Must be the parent model or directly related to it, on the same warehouse, and backed by a warehouse table
actionstringdelete_rows (physical DELETE) or null_columns (UPDATE … = NULL)
columnsarray of stringRequired for null_columns. May not include the join key to the parent model or the target’s primary key

Trigger a Run

POST /api/v1/workspaces/{id}/deletion-rules/{ruleId}/run POST /api/v1/workspaces/{id}/deletion-rules/{ruleId}/dry-run

Both return 202 Accepted with the opened run in running state — the deletion continues past the request, so poll the run log to follow it. /dry-run never mutates, whatever the rule’s own dry_run value. /run returns 400 when the rule is disabled.

Deletion Run Object

FieldTypeDescription
idstring (UUID)Unique identifier
rule_idstring (UUID) or nullSource rule. null once the rule has been deleted
workspace_idstring (UUID)Owning workspace
rule_namestringRule name snapshotted at run time
rule_snapshotobjectParent model, filter tree, targets and schedule as they were at run time
trigger_typestringmanual or schedule
modestringdry_run or execute
statusstringrunning, completed, partial (some targets failed), or failed
matched_countintegerRecords matched by the filter
records_affectedintegerRecords affected across all targets. In a dry run, records that would be
error_messagestringRun-level failure detail
initiated_bystring (UUID) or nullAccount that started the run
initiated_by_emailstringEmail snapshotted at run time, so the log stays attributable
started_atstring (ISO 8601)Start timestamp
completed_atstring (ISO 8601) or nullCompletion timestamp
targetsarrayPer-target outcomes — see below

Deletion Run Target Object

FieldTypeDescription
model_namestringTarget model name at run time
physical_tablestringFully qualified warehouse table
actionstringdelete_rows or null_columns
target_columnsarray of stringPhysical columns cleared
records_affectedintegerRecords affected. 0 on failure
statusstringcompleted, failed, or skipped
error_messagestringFailure detail for this target
statementstringThe statement executed — or, in a dry run, the statement that would be
executed_atstring (ISO 8601)When this target was processed
Last updated on