Admin API Endpoints
The admin API provides CRUD operations for managing tours, content, themes, audiences, analytics, and admin users. All endpoints require admin authentication (API key + JWT + admin strategy check).
Authentication
Every admin endpoint runs the require_admin dependency:
- API key resolves the tenant.
- JWT is validated against the tenant's JWKS.
- The tenant's configured admin strategy determines whether the user is an admin.
Requests from non-admin users receive 403 Forbidden.
# All admin requests require both headers:
-H "X-API-Key: sk_live_..."
-H "Authorization: Bearer <admin-jwt>"Tours
List Tours
GET /v1/admin/tours?limit=20&offset=0| Parameter | Type | Default | Description |
|---|---|---|---|
limit | int | 20 | Items per page (1-100) |
offset | int | 0 | Pagination offset |
Response 200 OK
{
"data": {
"items": [
{
"tour_id": "tour_456",
"title": "Analytics Dashboard Tour",
"status": "published",
"version": 3,
"step_count": 5,
"created_at": "2025-05-01T00:00:00Z",
"updated_at": "2025-05-10T14:22:00Z"
}
],
"total": 12,
"limit": 20,
"offset": 0
}
}Create Tour
POST /v1/admin/tours{
"title": "Getting Started",
"description": "Onboarding tour for new users",
"url_patterns": [
{ "pattern": "/dashboard*", "is_regex": false }
],
"steps": [
{
"step_id": "welcome",
"order": 0,
"title": "Welcome",
"body": "Welcome to the dashboard.",
"element_target": {
"data_attribute": "main-header"
},
"position": "bottom"
}
],
"trigger": {
"trigger_type": "page_load",
"delay_ms": 1000
},
"audience_id": null,
"rollout": { "enabled": true, "percentage": 100 },
"schedule": { "start_at": null, "end_at": null }
}Response 201 Created -- returns the full tour object with generated tour_id.
Each step requires step_id (any unique string), order (zero-based integer), title, body, and element_target (must include at least one of data_attribute / element_id / css_selector; selector is accepted as an alias for css_selector).
Trigger types (trigger.trigger_type, alias trigger.type): page_load · manual · first_visit · event · delay · element_visible · element_click · hover · scroll_depth · idle. See backend/src/spotlight/config/constants.py:TriggerType for the canonical list.
Audit event: tour.create
Get Tour
GET /v1/admin/tours/{tour_id}Response 200 OK -- full tour object including all steps and configuration.
Update Tour
PUT /v1/admin/tours/{tour_id}Accepts a partial update -- only provided fields are modified. The updated_at timestamp is set automatically.
Audit event: TourUpdatedByAdmin (includes before/after snapshots)
Publish Tour
POST /v1/admin/tours/{tour_id}/publishCreates an immutable version in Spotlight.Tours.Versions and sets the tour status to published. The tour becomes visible to end users matching the targeting criteria.
Response 200 OK
{
"data": {
"tour_id": "tour_456",
"status": "published",
"version": 4,
"published_at": "2025-05-10T15:00:00Z"
}
}Audit event: TourPublishedByAdmin
Archive Tour
POST /v1/admin/tours/{tour_id}/archiveRemoves the tour from user-facing content. Archived tours cannot be edited but can be duplicated.
Audit event: TourArchivedByAdmin
Copy Tour
POST /v1/admin/tours/{tour_id}/copyCreates a new draft tour with the same configuration as the source. The new tour gets a fresh tour_id.
Audit event: TourCopiedByAdmin
Version History
GET /v1/admin/tours/{tour_id}/versionsReturns all published versions for the tour, ordered by version number descending.
Rollback Version
POST /v1/admin/tours/{tour_id}/rollback{
"version": 2
}Restores the tour definition to a previous version and publishes it as a new version.
Audit event: TourVersionRolledBackByAdmin
Content
Non-tour content items: tooltips, spotlights, banners, modals.
List Content
GET /v1/admin/content?limit=20&offset=0Create Content
POST /v1/admin/content{
"content_type": "tooltip",
"title": "New Feature Alert",
"body": "Check out our new reporting tools.",
"url_patterns": [
{ "pattern": "/reports*", "is_regex": false }
],
"element_target": {
"element_id": "reports-tab"
},
"priority": 100,
"dismiss_behavior": "once",
"cta_text": "Learn More",
"cta_url": "/docs/reports",
"trigger": {
"trigger_type": "page_load",
"delay_ms": 500
},
"rollout": { "enabled": true, "percentage": 100 },
"schedule": { "start_at": null, "end_at": null }
}Response 201 Created -- returns the full content object with generated content_id (format: cnt_{12-char-hex}).
Audit event: ContentCreatedByAdmin
Get Content
GET /v1/admin/content/{content_id}Update Content
PUT /v1/admin/content/{content_id}Partial updates supported. Accepts any combination of fields.
Audit event: ContentUpdatedByAdmin
Publish Content
POST /v1/admin/content/{content_id}/publishSets status to published, making the content visible to end users.
Audit event: ContentPublishedByAdmin
Archive Content
POST /v1/admin/content/{content_id}/archiveAudit event: ContentArchivedByAdmin
Themes
List Themes
GET /v1/admin/themesCreate Theme
POST /v1/admin/themes{
"name": "Ember",
"variables": {
"--spot-bg": "#15151d",
"--spot-primary": "#ff6a4d",
"--spot-text": "#f8fafc",
"--spot-radius": "10px"
}
}variables is a flat map of CSS custom properties (the --spot-* design-token set the SDK reads at render time). Call GET /v1/admin/themes/presets to see a built-in palette to start from, or GET /v1/admin/themes/{theme_id} on any existing theme to discover the full token set. Omit variables to seed with the default Indigo palette and tweak via PUT below.
light / dark variant objects are accepted for explicit light + dark token sets; variables alone implies the light variant only.
Audit event: theme.create
Update Theme
PUT /v1/admin/themes/{theme_id}Body shape mirrors create — pass variables (flat map) or light / dark (variant objects) to replace the corresponding token set. Omit a field to leave it unchanged.
Audit event: theme.update
Set Default Theme
POST /v1/admin/themes/{theme_id}/set-defaultMarks this theme as the active palette for the tenant. The SDK on every page picks it up on the next page load. Unsetting happens automatically when another theme is set as default.
Audit event: theme.set_default
Delete Theme
DELETE /v1/admin/themes/{theme_id}Returns 204 No Content on success. Refuses with 409 Conflict when:
- The theme is the tenant's current default (
set-defaultanother theme first). - The theme is a shared / built-in preset (
is_shared: true) — these are read-only.
Audit event: theme.deleted
Audiences
Audience rules live inline on each piece of content via the audience field on tours, modals, banners, hotspots, spotlights, tooltips, and checklists. See the relevant content-type page for the rule schema.
Analytics
Selector Health
GET /v1/admin/analytics/selector-health?window_days=7&failure_threshold=3See Element Selector: Selector Health Monitoring.
Per-tour and per-content analytics are surfaced in the dashboard under Analytics.
Admin Users
Manage admin access for the tenant (only when using the table_lookup admin strategy).
List Admin Users
GET /v1/admin/usersAdd Admin User
POST /v1/admin/users{
"user_id": "auth0|user_789",
"email": "admin@example.com",
"role": "admin",
"name": "Jane Doe"
}Audit event: AdminUserAddedByAdmin
Update Admin User
PUT /v1/admin/users/{user_id}Audit event: AdminUserUpdatedByAdmin
Remove Admin User
DELETE /v1/admin/users/{user_id}Audit event: AdminUserRemovedByAdmin
Admin Activity
Activity Log
GET /v1/admin/activity?limit=50Returns the admin's recent activity from the Audit.AdminActions table.
Activity by Entity
GET /v1/admin/activity/entity/{entity_type}/{entity_id}Returns the full change history for a specific entity (tour, content, theme, etc.).
Configuration
Get Tenant Config
GET /v1/admin/configReturns the tenant's configuration including admin strategy, allowed origins, and feature flags.
Update Tenant Config
PUT /v1/admin/configToggle Circuit Breaker
POST /v1/admin/config/circuit-breaker{
"feature": "tours",
"enabled": false
}Emergency kill switch that disables all tours or content for the tenant without archiving individual items.
Audit event: CircuitBreakerToggledByAdmin
Error Responses
All admin endpoints return errors in the standard envelope:
{
"error": {
"code": "NOT_FOUND",
"message": "Content not found",
"request_id": "req_abc123"
}
}| Status | Code | Description |
|---|---|---|
400 | VALIDATION_ERROR | Invalid request body |
401 | UNAUTHORIZED | Missing or invalid credentials |
403 | FORBIDDEN | User is not an admin |
404 | NOT_FOUND | Resource does not exist |
409 | CONFLICT | Resource state conflict (e.g., publishing a published tour) |
422 | UNPROCESSABLE_ENTITY | Valid JSON but invalid data |
429 | RATE_LIMITED | Too many requests |
500 | INTERNAL_ERROR | Server error |