Recertification API
Manage recertification cycles and access audits for periodic review of data products and source system roles.
All cycle endpoints are scoped to a space via the
{slug}path parameter. Most write operations require Space Admin permissions.
Endpoints Overview
Cycle CRUD
| Method | Endpoint | Description |
|---|---|---|
POST | /spaces/{slug}/governance/cycles | Create a cycle |
GET | /spaces/{slug}/governance/cycles | List cycles |
GET | /spaces/{slug}/governance/cycles/{cycle_id} | Get cycle detail |
DELETE | /spaces/{slug}/governance/cycles/{cycle_id} | Delete a cycle |
Cycle Actions
| Method | Endpoint | Description |
|---|---|---|
POST | .../cycles/{cycle_id}/populate | Auto-populate audit items |
POST | .../cycles/{cycle_id}/complete | Mark as completed |
POST | .../cycles/{cycle_id}/cancel | Cancel a cycle |
POST | .../cycles/{cycle_id}/reopen | Reopen a cancelled cycle |
POST | .../cycles/{cycle_id}/archive | Archive a cycle |
POST | .../cycles/{cycle_id}/unarchive | Unarchive a cycle |
Cycle Configuration
| Method | Endpoint | Description |
|---|---|---|
PATCH | .../cycles/{cycle_id}/workflow | Set/clear workflow definition |
PATCH | .../cycles/{cycle_id}/config | Update templates and filters |
GET | /spaces/{slug}/governance/filter-suggestions | Autocomplete values for filters |
Audit Operations
| Method | Endpoint | Description |
|---|---|---|
POST | /governance/audits | Create an audit record |
PATCH | /governance/audits/{audit_id} | Review an audit item |
POST | /spaces/{slug}/governance/audits/{audit_id}/recertify | Create a recertification request |
POST | .../cycles/{cycle_id}/recertify-all | Bulk recertify all pending items |
Workflow Defaults
| Method | Endpoint | Description |
|---|---|---|
GET | /spaces/{slug}/governance/workflow-defaults | List workflow defaults |
POST | /spaces/{slug}/governance/workflow-defaults | Create a workflow default |
PATCH | .../workflow-defaults/{default_id} | Update a workflow default |
DELETE | .../workflow-defaults/{default_id} | Delete a workflow default |
Recertification Cycles
Create Cycle
POST /spaces/{slug}/governance/cycles
Requires: Space Admin
Request Body
{
"name": "Q1 2026 PII Review",
"due_date": "2026-03-31T23:59:59Z",
"cycle_type": "product",
"description": "Quarterly review of PII-tagged data products."
}
| Field | Type | Required | Description |
|---|---|---|---|
name | string | ✅ | Cycle name |
due_date | datetime | ✅ | Review deadline |
cycle_type | string | ✅ | product or access |
description | string | — | Optional description |
Response
{
"id": "uuid",
"name": "Q1 2026 PII Review",
"cycle_type": "product",
"status": "open",
"due_date": "2026-03-31T23:59:59Z",
"start_date": "2026-02-10T10:00:00Z",
"completed_at": null,
"space_id": "uuid",
"created_by_id": "uuid",
"is_archived": false,
"workflow_definition_id": null,
"request_title_template": null,
"request_description_template": null,
"filters": null,
"audit_count": 0,
"reviewed_count": 0
}
List Cycles
GET /spaces/{slug}/governance/cycles?include_archived=false
| Parameter | Type | Default | Description |
|---|---|---|---|
include_archived | bool | false | Include archived cycles |
Returns: RecertificationCycleResponse[]
Get Cycle Detail
GET /spaces/{slug}/governance/cycles/{cycle_id}
Returns the cycle with all embedded audit items, audit counts, templates, filters, and workflow configuration.
Returns: RecertificationCycleDetail (extends RecertificationCycleResponse with an audits array)
Delete Cycle
DELETE /spaces/{slug}/governance/cycles/{cycle_id}
Requires: Space Admin
Permanently deletes the cycle and all its audit items. Returns 204 No Content.
Cycle Actions
Populate
POST /spaces/{slug}/governance/cycles/{cycle_id}/populate
Requires: Space Admin
Scans the space for resources matching the cycle's filters and creates audit items. Idempotent — re-populating skips resources that already have an audit row.
Response
{
"created": 42
}
Complete
POST /spaces/{slug}/governance/cycles/{cycle_id}/complete
Requires: Space Admin
Transitions the cycle to completed status (read-only).
Cancel
POST /spaces/{slug}/governance/cycles/{cycle_id}/cancel
Requires: Space Admin
Aborts the cycle. Can be reopened later.
Reopen
POST /spaces/{slug}/governance/cycles/{cycle_id}/reopen
Requires: Space Admin
Reopens a cancelled cycle to resume the review process.
Archive / Unarchive
POST /spaces/{slug}/governance/cycles/{cycle_id}/archive
POST /spaces/{slug}/governance/cycles/{cycle_id}/unarchive
Requires: Space Admin
Toggles the cycle's visibility in the default list view.
Cycle Configuration
Update Workflow
PATCH /spaces/{slug}/governance/cycles/{cycle_id}/workflow
Requires: Space Admin
{
"workflow_definition_id": "uuid-or-null"
}
Set a workflow definition for the cycle. Recertification requests created from this cycle will be routed through the specified workflow. Pass null to clear.
Update Config (Templates & Filters)
PATCH /spaces/{slug}/governance/cycles/{cycle_id}/config
Requires: Space Admin
{
"request_title_template": "Recertify {{ product_name }} ({{ product_type }})",
"request_description_template": "## Review for {{ product_name }}\n\n**Criticality:** {{ criticality }}",
"filters": {
"tags": ["pii", "gdpr"],
"criticality": ["High", "Critical"]
}
}
All fields are optional — only provided fields are updated.
Template Variables
The following Jinja2 variables are available in title and description templates:
Common variables (all cycle types):
| Variable | Source | Description |
|---|---|---|
resource_name | Audit | Display name of the resource |
resource_type | Audit | Resource type (e.g., Table) |
cycle_name | Cycle | Name of the parent cycle |
space_name | Space | Name of the space |
Product cycle variables:
| Variable | Source | Description |
|---|---|---|
product_name | Product | Data product name |
product_type | Product | Product type |
criticality | Product | Criticality level |
sensitivity | Product | Sensitivity classification |
provider | Product | Data provider |
Access cycle variables (SourceSystemRole audits):
| Variable | Type | Description |
|---|---|---|
role_name | string | Role name |
source_system_name | string | Source system the role belongs to |
role_access_list | list[dict] | Users with active access — each dict has user_name, user_email, product_name, granted_at |
role_access_table | string | Pre-rendered markdown table of users with active access |
Example: access table in description
request_description_template: |
## Users with access to {{ role_name }}
{{ role_access_table }}
Renders to:
## Users with access to Analyst PII Role
| User | Email | Product | Granted |
|---|---|---|---|
| Alice Smith | alice@example.com | MySnowFlakeDB | 2026-01-15 |
| Bob Jones | bob@example.com | MySnowFlakeDB | 2026-02-01 |
Filter Suggestions
GET /spaces/{slug}/governance/filter-suggestions
Returns distinct values available in the space for autocomplete.
{
"product_types": ["Table", "Dashboard", "Report"],
"criticalities": ["Critical", "High", "Medium", "Low"],
"tags": ["pii", "gdpr", "financial"]
}
Audit Operations
Create Audit
POST /governance/audits
Manually create a single audit record (usually automated by cycle population).
{
"cycle_id": "uuid",
"resource_id": "uuid",
"resource_name": "customer_orders",
"resource_type": "Table"
}
Review Audit
PATCH /governance/audits/{audit_id}
Approve or reject an audit item.
{
"status": "approved",
"comments": "Access is still required for daily operations."
}
| Field | Type | Description |
|---|---|---|
status | string | approved or rejected |
comments | string | Optional reviewer notes |
Create Recertification Request
POST /spaces/{slug}/governance/audits/{audit_id}/recertify
Creates a recertification request linked to the audit item. The request title and description are rendered from the cycle's Jinja2 templates (if configured).
Optional Body
{
"description": "Fallback description if no template is configured."
}
Response
Returns the created request object.
Bulk Recertify
POST /spaces/{slug}/governance/cycles/{cycle_id}/recertify-all
Requires: Space Admin
Creates recertification requests for all pending audit items in the cycle. Skips items that already have an active request.
{
"created": 38,
"skipped": 4
}
Response Models
RecertificationCycleResponse
| Field | Type | Description |
|---|---|---|
id | UUID | Cycle identifier |
name | string | Cycle name |
description | string | null | Optional description |
cycle_type | string | product or access |
status | string | open, in_review, completed, cancelled |
due_date | datetime | Review deadline |
start_date | datetime | When the cycle was created |
completed_at | datetime | null | When the cycle was completed |
space_id | UUID | Space the cycle belongs to |
created_by_id | UUID | null | User who created the cycle |
is_archived | boolean | Whether the cycle is hidden from default views |
workflow_definition_id | UUID | null | Linked workflow definition |
request_title_template | string | null | Jinja2 template for request titles |
request_description_template | string | null | Jinja2 template for request descriptions |
filters | object | null | Population filter configuration |
audit_count | integer | Total audit items |
reviewed_count | integer | Decided audit items |
RecertificationCycleDetail
Extends RecertificationCycleResponse with:
| Field | Type | Description |
|---|---|---|
audits | RecertificationAuditResponse[] | Embedded audit items |
RecertificationAuditResponse
| Field | Type | Description |
|---|---|---|
id | UUID | Audit identifier |
cycle_id | UUID | Parent cycle |
resource_id | UUID | Resource being reviewed |
resource_name | string | null | Resource display name |
resource_type | string | null | Resource type (e.g., Table, Dashboard, SourceSystemRole) |
status | string | pending, approved, rejected, expired |
reviewer_id | UUID | null | Who made the decision |
reviewed_at | datetime | null | When the decision was made |
comments | string | null | Reviewer comments |
product_id | string | null | Associated data product ID (product cycles) |
source_system_id | string | null | Associated source system ID (access cycles) |
has_pending_request | boolean | Whether a recertification request is in-flight |
request_id | string | null | ID of the linked recertification request |
Enumerations
RecertificationCycleType
| Value | Description |
|---|---|
product | Reviews data products |
access | Reviews source system roles |
RecertificationCycleStatus
| Value | Description |
|---|---|
open | Cycle is active and accepting reviews |
in_review | Cycle has been populated and reviews are in progress |
completed | All reviews are done (read-only) |
cancelled | Cycle was cancelled (can be reopened) |
RecertificationStatus (Audit)
| Value | Description |
|---|---|
pending | Awaiting review |
approved | Reviewer approved continued access |
rejected | Reviewer rejected continued access |
expired | Review window expired without action |
Workflow Defaults
Workflow defaults define which workflow definition is automatically assigned to new recertification cycles based on cycle type and optional conditions (e.g., tags). Higher-priority defaults are evaluated first; the first match wins. If no conditional default matches, the unconditional default (no conditions) is used as a fallback.
List Defaults
GET /spaces/{slug}/governance/workflow-defaults?cycle_type=access
| Parameter | Type | Default | Description |
|---|---|---|---|
cycle_type | string | — | Optional filter: product or access |
Returns: RecertWorkflowDefaultResponse[]
Create Default
POST /spaces/{slug}/governance/workflow-defaults
Requires: Space Admin
{
"cycle_type": "access",
"workflow_definition_id": "uuid",
"conditions": {"tags": ["pii"]},
"priority": 10,
"name": "PII Access Workflow"
}
| Field | Type | Required | Description |
|---|---|---|---|
cycle_type | string | ✅ | product or access |
workflow_definition_id | UUID | ✅ | Workflow to assign |
conditions | object | — | Match conditions (e.g., {"tags": [...]}) |
priority | integer | — | Higher values are evaluated first (default: 0) |
name | string | — | Human-readable label |
Returns: RecertWorkflowDefaultResponse (201 Created)
Update Default
PATCH /spaces/{slug}/governance/workflow-defaults/{default_id}
Requires: Space Admin
All fields are optional — only provided fields are updated.
{
"priority": 20,
"name": "Updated PII Workflow"
}
Returns: RecertWorkflowDefaultResponse
Delete Default
DELETE /spaces/{slug}/governance/workflow-defaults/{default_id}
Requires: Space Admin
Returns 204 No Content.
RecertWorkflowDefaultResponse
| Field | Type | Description |
|---|---|---|
id | UUID | Default identifier |
space_id | UUID | Space the default belongs to |
cycle_type | string | product or access |
workflow_definition_id | UUID | Linked workflow definition |
conditions | object | null | Match conditions |
priority | integer | Evaluation priority (higher = first) |
name | string | null | Display name |
created_at | datetime | When the default was created |
Webhook Events
The following webhook events are dispatched during recertification and access management operations. Subscribe to these events via the Webhooks API.
Recertification Events
| Event Type | Trigger | Payload |
|---|---|---|
recertification.cycle_created | New cycle created | cycle_id, space_id, cycle_type, name |
recertification.cycle_completed | Cycle marked as completed | cycle_id, space_id, reviewed_count, audit_count |
recertification.access_revoked | Access revoked during recertification review | audit_id, cycle_id, resource_id, user_id |
Auto-Revoke Events
| Event Type | Trigger | Payload |
|---|---|---|
access.auto_revoked | User deactivated (admin, SCIM, or system) | user_id, source, revoked_count, product_ids |
The access.auto_revoked event is dispatched per space — if a user had access to products in 3 different spaces, 3 separate webhook events are fired, each containing only the product IDs from that space.
Example Webhook Payload
{
"event_type": "access.auto_revoked",
"space_id": "uuid",
"timestamp": "2026-02-14T15:30:00Z",
"payload": {
"user_id": "uuid",
"source": "auto_departed",
"revoked_count": 3,
"product_ids": ["uuid-1", "uuid-2", "uuid-3"]
}
}
Auto-Revoke on User Deactivation
When a user is deactivated — whether via the admin panel, SCIM provisioning, or programmatically — all their active product access is immediately and automatically revoked.
How It Works
- User deactivated →
AutoRevokeServicefinds all activeProductAccessrows - All grants are bulk-updated:
is_active = false,revocation_reasonset to the source - Audit trail entries created for each revoked grant (
access.auto_revoked) - Webhook events dispatched to each affected space
Revocation Sources
| Source | Description |
|---|---|
auto_departed | User departure detected (SCIM deprovision, admin toggle) |
admin | Manual admin deactivation |
recertification | Revoked as part of recertification review |
Database Schema
The product_access table includes a revocation_reason column:
| Column | Type | Description |
|---|---|---|
revocation_reason | VARCHAR | Why the access was revoked (auto_departed, admin, recertification, etc.) |
Smart Recertification
Smart Recertification enriches audit items with usage signals and generates automated recommendations, reducing the time reviewers spend on obvious access decisions.
Analyze Cycle
POST /spaces/{slug}/recertification/cycles/{cycle_id}/analyze
Runs the smart analysis engine on all audits in a cycle. The engine:
- Loads the exclusion list and skips excluded users
- Builds signal maps (user status, access history, role changes)
- Enriches each audit item with signals
- Applies rule-based recommendations
- Persists results to the database
Response: 200 OK
{
"approve": 83,
"revoke": 12,
"uncertain": 5,
"excluded": 3,
"total": 103
}
Errors:
400 Bad Request— Cycle not found or not in an active state
Signal Enrichment
Each audit item is annotated with the following signal columns:
| Signal | Type | Description |
|---|---|---|
user_active | bool | Whether the user account is currently active |
last_accessed_at | datetime | null | Last access timestamp |
days_since_grant | int | Days since the access was granted |
role_changed | bool | Whether the user's role changed since the grant |
role_at_grant | string | null | The user's role when access was originally granted |
Recommendation Rules
Rules are evaluated in priority order (highest priority first, skipped when disabled):
| Rule | Recommendation | Confidence | Toggle |
|---|---|---|---|
| Departed user (deactivated > 90 days) | Revoke | High | departed_user_enabled |
| Departed user (recently deactivated) | Revoke | Medium | departed_user_enabled |
| Inactive grant (no access > 180 days) | Revoke | Medium | — |
| Inactive grant (no access > 90 days) | Revoke | Low | — |
| Role mismatch since grant | Revoke | Medium | — |
| Recently active with matching role | Approve | High | — |
| Active user with recent access (< 30 days) | Approve | High | — |
| Active user with moderate access (< 90 days) | Approve | Medium | — |
Bulk Apply Recommendations
POST /spaces/{slug}/recertification/cycles/{cycle_id}/bulk-apply
Apply matching recommendations as reviews in bulk.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
recommendation | string | ✅ | approve or revoke |
min_confidence | string | — | Minimum confidence: high (default), medium, low |
reviewer_id | UUID | — | Override reviewer (defaults to caller) |
Response: 200 OK
{
"applied": 75,
"skipped": 8
}
Exclusion List
Users on the exclusion list are skipped during cycle population and smart analysis.
List Exclusions
GET /spaces/{slug}/recertification/exclusions
Response: 200 OK — Array of exclusion entries
Add Exclusion
POST /spaces/{slug}/recertification/exclusions
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
user_id | UUID | ✅ | User to exclude |
reason | string | — | Reason for exclusion (e.g., "Service account") |
Response: 201 Created
Remove Exclusion
DELETE /spaces/{slug}/recertification/exclusions/{user_id}
Response: 204 No Content
Rule Configuration
Space-level configuration for smart recertification thresholds and toggles.
Get Rule Config
GET /spaces/{slug}/recertification/rule-config
Response: 200 OK
{
"departed_user_enabled": true,
"inactive_threshold_days": 180,
"moderate_threshold_days": 90,
"recent_threshold_days": 30,
"auto_revoke_departed": true,
"auto_revoke_grace_days": 30
}
Update Rule Config
PATCH /spaces/{slug}/recertification/rule-config
Request Body (all fields optional):
| Field | Type | Default | Description |
|---|---|---|---|
departed_user_enabled | bool | true | Enable departed user detection |
inactive_threshold_days | int | 180 | Days before a grant is considered inactive |
moderate_threshold_days | int | 90 | Days for moderate access classification |
recent_threshold_days | int | 30 | Days for recent access classification |
auto_revoke_departed | bool | true | Auto-revoke departed users |
auto_revoke_grace_days | int | 30 | Grace period for auto-revoke appeals |
Response: 200 OK — Updated config