REST API Reference
REST API Reference¶
The Huitzo REST API provides programmatic access to all platform capabilities.
Base URL¶
Production: https://huitzo.ai/v1
Self-hosted: http://your-server:8080/api/v1
Authentication¶
All API requests require authentication via Bearer token.
curl -H "Authorization: Bearer sk_xxxxx" \
https://huitzo.ai/v1/commands
Getting a Token¶
Via API:
POST /api/v1/auth/login
Content-Type: application/json
{
"email": "[email protected]",
"password": "your-password"
}
Response:
{
"data": {
"tokens": {
"accessToken": "eyJhbG...",
"refreshToken": "eyJhbG...",
"expiresAt": 1740000000000
},
"user": {
"id": "usr_abc123",
"email": "[email protected]",
"name": "user",
"role": "owner",
"developerMode": false,
"subscriptionTier": "developer_preview",
"createdAt": "2026-02-15T10:00:00Z"
}
}
}
Response Format¶
Success Response¶
All successful API responses wrap the payload in a data envelope. SDK clients must unwrap this envelope to access the actual response data.
{
"data": { ... },
"meta": {
"request_id": "req_abc123",
"timestamp": "2025-01-22T10:30:00Z"
}
}
The data field always contains the response payload. The meta field is optional and omitted by most endpoints; it is included only when the server has additional context to convey. The Dashboard SDK handles this unwrapping automatically.
Error Response¶
All error responses follow a consistent structure aligned with the SDK:
{
"success": false,
"error": {
"type": "ValidationError",
"message": "Invalid input: email format is incorrect",
"code": "VALIDATION_FAILED",
"details": {
"field": "email",
"value": "not-an-email",
"issue": "invalid format"
},
"correlation_id": "req_abc123",
"timestamp": "2026-01-22T10:30:00Z"
}
}
| Field | Type | Description |
|---|---|---|
success |
boolean | Always false for errors |
error.type |
string | Exception class name (e.g., ValidationError, TimeoutError) |
error.message |
string | Human-readable error description |
error.code |
string | Machine-readable error code (see table below) |
error.details |
object | Additional context (field-specific errors, etc.) |
error.correlation_id |
string | Unique request ID for debugging |
error.timestamp |
string | ISO 8601 timestamp |
Error Codes¶
| Code | HTTP Status | Description |
|---|---|---|
VALIDATION_FAILED |
400 | Invalid input data |
AUTHENTICATION_FAILED |
401 | Invalid or missing token |
PERMISSION_DENIED |
403 | Insufficient permissions |
NOT_FOUND |
404 | Resource not found |
TIMEOUT |
408 | Request timeout exceeded |
RATE_LIMITED |
429 | Too many requests |
INTERNAL_ERROR |
500 | Unexpected server error |
INTEGRATION_ERROR |
502 | External service failure |
SERVICE_UNAVAILABLE |
503 | Service temporarily unavailable |
Endpoints¶
Authentication¶
Register¶
Create a new user account. Requires a valid access code. All accounts start in Consumer mode.
POST /api/v1/auth/register
Request:
{
"email": "[email protected]",
"password": "securepassword",
"slug": "jane-acme",
"access_code": "HUITZO-A1B2-C3D4"
}
| Field | Type | Required | Description |
|---|---|---|---|
email |
string | Yes | User email address |
password |
string | Yes | Password (min 8 characters) |
slug |
string | Yes | Personal namespace slug (2-39 chars, lowercase, no reserved prefixes) |
access_code |
string | Yes | Valid access code (format: HUITZO-XXXX-XXXX) |
Response:
{
"data": {
"tokens": {
"accessToken": "eyJhbG...",
"refreshToken": "eyJhbG...",
"expiresAt": 1740000000000
},
"user": {
"id": "usr_abc123",
"email": "[email protected]",
"name": "jane",
"role": "owner",
"developerMode": false,
"subscriptionTier": "developer_preview",
"createdAt": "2026-02-15T10:00:00Z"
}
}
}
Login¶
POST /api/v1/auth/login
Request:
{
"email": "[email protected]",
"password": "your-password"
}
Response:
{
"data": {
"tokens": {
"accessToken": "eyJhbG...",
"refreshToken": "eyJhbG...",
"expiresAt": 1740000000000
},
"user": {
"id": "usr_abc123",
"email": "[email protected]",
"name": "user",
"role": "owner",
"developerMode": false,
"subscriptionTier": "developer_preview",
"createdAt": "2026-01-01T00:00:00Z"
}
}
}
Refresh Token¶
POST /api/v1/auth/refresh
Request:
{
"refreshToken": "eyJhbG..."
}
Response:
{
"data": {
"accessToken": "eyJhbG...",
"refreshToken": "eyJhbG...",
"expiresAt": 1740000000000
}
}
Refresh Token (Cookie-Based)¶
Used by the Hub dashboard for XSS-safe token refresh. Reads the refresh token from the huitzo_refresh HttpOnly cookie instead of the request body. CLI/SDK should use the body-based POST /auth/refresh endpoint above.
POST /api/v1/auth/refresh-cookie
Cookie: huitzo_refresh=<refresh-token>
No request body required. The refresh token is read from the HttpOnly cookie set during login or previous refresh.
Response:
{
"data": {
"accessToken": "eyJhbG...",
"expiresAt": 1740000000000
}
}
The response also sets a new huitzo_refresh HttpOnly cookie with a rotated refresh token.
This endpoint is public (no Bearer token required) since its purpose is to obtain a new access token when the previous one has expired. The cookie is scoped to the /api/v1/auth path prefix, so the browser does not send it on other API requests.
Logout¶
POST /api/v1/auth/logout
Authorization: Bearer {token}
Response:
{
"data": {
"message": "Successfully logged out"
}
}
Change Password (Authenticated)¶
Change password from an active session (Hub Settings page).
POST /api/v1/auth/change-password
Authorization: Bearer {token}
Request:
{
"current_password": "old-password",
"new_password": "new-password"
}
Response:
{
"data": {
"message": "Password updated successfully"
}
}
On success, all existing refresh sessions are revoked and a password-change notification email is sent (best effort) through SendGrid.
Change Password (Login Screen)¶
Change password from the login screen using email plus the current password.
POST /api/v1/auth/change-password/login
Request:
{
"email": "[email protected]",
"current_password": "old-password",
"new_password": "new-password"
}
Response:
{
"data": {
"message": "Password updated successfully"
}
}
This endpoint is public because it is designed for the login page flow. It still requires valid current credentials and sends the same account-change email notification.
Account¶
Get Account Mode¶
Check the current account mode and organization membership.
GET /api/v1/account/mode
Authorization: Bearer {token}
Response (Consumer Mode):
{
"data": {
"account_mode": "consumer",
"developer_mode_available": true
}
}
Response (Developer Mode):
{
"data": {
"account_mode": "developer",
"organization": {
"id": "org_xyz789",
"name": "Acme Corp",
"slug": "acme",
"scope": "@acme",
"role": "owner"
}
}
}
Enable Developer Mode¶
Enable Developer Mode for the current user. Creates a new organization or joins an existing one via invite.
POST /api/v1/account/developer-mode
Authorization: Bearer {token}
Content-Type: application/json
Request (Create New Organization):
{
"organization": {
"name": "Acme Corp",
"slug": "acme"
}
}
Request (Join via Access Code):
{
"access_code": "inv_abc123xyz"
}
Response:
{
"data": {
"user": {
"id": "usr_abc123",
"email": "[email protected]",
"mode": "developer"
},
"organization": {
"id": "org_xyz789",
"name": "Acme Corp",
"slug": "acme",
"scope": "@acme"
},
"message": "Developer Mode enabled. You can now use the SDK and CLI."
}
}
Error Responses:
| Code | Error | Description |
|---|---|---|
| 400 | SLUG_TAKEN |
Organization slug already in use |
| 400 | SLUG_INVALID |
Slug doesn't meet validation rules |
| 400 | SLUG_RESERVED |
Slug is a reserved scope |
| 404 | INVITE_NOT_FOUND |
Access code is invalid or expired |
Slug Validation Rules:
- 2-39 characters
- Lowercase letters, numbers, hyphens only
- Cannot start or end with hyphen
- Cannot be reserved (huitzo, system, admin)
API Keys¶
Machine-to-machine authentication for CI/CD pipelines and scripts. Keys use the
format sk-huitzo-<64 hex chars> (256-bit entropy), hashed with argon2id at rest.
Auth: All endpoints require JWT. API keys cannot manage other API keys.
Create API Key¶
POST /api/v1/account/api-keys
Authorization: Bearer {jwt_token}
Content-Type: application/json
# pseudocode — request
{ "name": "descriptive-name", "scopes": ["commands:execute"] }
# pseudocode — response (201, plaintext key returned exactly once)
{
"data": {
"id": "uuid",
"name": "descriptive-name",
"key": "sk-huitzo-<64 hex chars>",
"key_prefix": "sk-huitzo-abcd12",
"scopes": ["commands:execute"],
"created_at": "ISO8601"
}
}
Allowed scopes: commands:execute (only value accepted in v1).
List API Keys¶
GET /api/v1/account/api-keys
Authorization: Bearer {jwt_token}
Returns metadata only — never returns the hashed secret.
# pseudocode — response fields per key
{ "id", "name", "key_prefix", "scopes", "last_used_at", "revoked_at", "created_at" }
Revoke API Key¶
Soft-delete — sets revoked_at, does not hard-delete the row.
DELETE /api/v1/account/api-keys/{key_id}
Authorization: Bearer {jwt_token}
| Code | Condition |
|---|---|
| 200 | Key revoked |
| 403 | Request came via API key (keys cannot revoke keys) |
| 404 | Key not found or belongs to another user |
Organizations¶
Create Organization¶
Create a new organization (requires Developer Mode).
POST /api/v1/organizations
Authorization: Bearer {token}
Content-Type: application/json
Request:
{
"name": "New Organization",
"slug": "new-org"
}
Response:
{
"data": {
"organization": {
"id": "org_new123",
"name": "New Organization",
"slug": "new-org",
"scope": "@new-org",
"createdAt": "2026-01-24T10:30:00Z"
}
}
}
Get Organization¶
GET /api/v1/organizations/{org_id}
Authorization: Bearer {token}
Response:
{
"data": {
"organization": {
"id": "org_xyz789",
"name": "Acme Corp",
"slug": "acme",
"scope": "@acme",
"createdAt": "2026-01-01T00:00:00Z",
"members_count": 3,
"packs_count": 5
}
}
}
Invite Member¶
Invite a user to join the organization.
POST /api/v1/organizations/{org_id}/invite
Authorization: Bearer {token}
Content-Type: application/json
Request:
{
"email": "[email protected]",
"role": "member"
}
Response:
{
"data": {
"invite": {
"id": "inv_abc123xyz",
"email": "[email protected]",
"role": "member",
"expiresAt": "2026-02-07T10:30:00Z"
},
"message": "Invitation sent to [email protected]"
}
}
Available Roles:
| Role | Permissions |
|---|---|
owner |
Full control, billing, delete org |
admin |
Manage members, publish packs |
member |
Publish packs under org scope |
List Members¶
GET /api/v1/organizations/{org_id}/members
Authorization: Bearer {token}
Response:
{
"data": [
{
"id": "usr_abc123",
"user_id": "usr_user123",
"email": "[email protected]",
"role": "owner",
"joined_at": "2026-01-01T00:00:00Z"
},
{
"id": "usr_def456",
"user_id": "usr_user456",
"email": "[email protected]",
"role": "admin",
"joined_at": "2026-01-15T00:00:00Z"
}
]
}
Remove Member¶
DELETE /api/v1/organizations/{org_id}/members/{user_id}
Authorization: Bearer {token}
Response:
{
"data": {
"message": "Member removed from organization"
}
}
Commands¶
List Commands¶
Get all available commands for the current tenant.
GET /api/v1/commands
Authorization: Bearer {token}
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
namespace |
string | - | Filter by namespace |
search |
string | - | Search by name/description |
page |
integer | 1 | Page number |
limit |
integer | 20 | Items per page |
Response:
{
"data": {
"commands": [
{
"namespace": "financial",
"name": "analyze",
"version": "2.0.0",
"description": "Analyze portfolio performance",
"input_schema": {
"type": "object",
"properties": {
"symbol": { "type": "string" },
"period": { "type": "string", "enum": ["1d", "1w", "1m", "1y"] }
},
"required": ["symbol"]
},
"output_format": "json",
"timeout": 60,
"tags": ["finance", "analysis"]
}
],
"total": 15,
"page": 1,
"pages": 1
}
}
Get Command¶
Get details for a specific command.
GET /api/v1/commands/{namespace}/{name}
Authorization: Bearer {token}
Response:
{
"data": {
"namespace": "financial",
"name": "analyze",
"version": "2.0.0",
"description": "Analyze portfolio performance",
"input_schema": { ... },
"output_format": "json",
"timeout": 60,
"retries": 3,
"tags": ["finance", "analysis"],
"examples": [
{
"description": "Analyze Apple stock",
"input": { "symbol": "AAPL", "period": "1m" }
}
]
}
}
Execute Command¶
Execute a command and get results.
POST /api/v1/commands/{namespace}/{name}
Authorization: Bearer {token}
Content-Type: application/json
Request:
{
"symbol": "AAPL",
"period": "1m"
}
Response (Sync - fast commands):
{
"data": {
"result": {
"symbol": "AAPL",
"performance": {
"return": 0.0842,
"volatility": 0.23,
"sharpe_ratio": 1.45
},
"recommendation": "Hold"
},
"execution": {
"duration_ms": 1234,
"status": "completed"
}
}
}
Response (Async - long-running commands):
{
"data": {
"task_id": "task_abc123",
"status": "pending",
"status_url": "/api/v1/tasks/task_abc123"
}
}
Tasks¶
Task polling endpoints allow clients to check the status of async command executions
and cancel running tasks. These are the primary integration surface for CI/CD
pipelines via the GitHub Action (run-pack-action@v1).
Auth: JWT or sk-huitzo-* API key. Caller must own the task (matched on
user_id AND tenant_id).
State resolution: DB is authoritative for terminal states; Celery
AsyncResult is checked for in-flight tasks (pending/started).
Get Task Status¶
GET /api/v1/tasks/{task_id}
Authorization: Bearer {token_or_api_key}
Status values: pending | started | success | failure | timeout | revoked
Response (success):
# pseudocode — illustrative shape, not copy-paste
{
"success": true,
"data": {
"task_id": "uuid",
"status": "success",
"result": { "...pack output..." },
"error": null,
"started_at": "ISO8601",
"completed_at": "ISO8601",
"correlation_id": "uuid"
}
}
Response (failure):
# pseudocode
{
"success": true,
"data": {
"task_id": "uuid",
"status": "failure",
"result": null,
"error": { "message": "human-readable", "type": "ExceptionClassName" },
"started_at": "ISO8601",
"completed_at": "ISO8601",
"correlation_id": "uuid"
}
}
Error responses:
| Code | Condition |
|---|---|
| 401 | Missing or invalid auth |
| 403 | Task belongs to a different user or tenant |
| 404 | Task ID not found |
| 410 | Task completed but result expired (never persisted to result_full) |
Cancel Task¶
Cancel a running task. Returns 409 Conflict if the task is already in a
terminal state (success, failure, timeout, revoked).
POST /api/v1/tasks/{task_id}/cancel
Authorization: Bearer {token_or_api_key}
Response (200):
# pseudocode
{
"success": true,
"data": {
"task_id": "uuid",
"revoked": true
}
}
Error responses:
| Code | Condition |
|---|---|
| 403 | Not your task |
| 404 | Task not found |
| 409 | Task already in terminal state |
---
### Registry
#### Grant Pack Access
Grant a specific tenant access to a restricted pack.
```http
POST /api/v1/registry/grants
Authorization: Bearer {token}
Content-Type: application/json
Request:
{
"pack_namespace": "huitzo-solutions",
"pack_name": "finance",
"grantee_tenant_id": "client-tenant-abc"
}
Response:
{
"data": {
"granted": true,
"timestamp": "2025-01-22T10:30:00Z"
}
}
Revoke Pack Access¶
DELETE /api/v1/registry/grants
Authorization: Bearer {token}
Content-Type: application/json
Request:
{
"pack_namespace": "huitzo-solutions",
"pack_name": "finance",
"grantee_tenant_id": "client-tenant-abc"
}
Packs¶
List Installed Packs¶
GET /api/v1/packs
Authorization: Bearer {token}
Response:
{
"data": {
"packs": [
{
"id": "pack_abc123",
"name": "financial-analysis",
"namespace": "financial",
"version": "2.0.0",
"commands": 3,
"installed_at": "2025-01-20T10:00:00Z",
"status": "active"
}
]
}
}
Install Pack¶
Install a pack from registry.
POST /api/v1/packs/install
Authorization: Bearer {token}
Content-Type: application/json
Request:
{
"name": "financial-analysis",
"version": "2.0.0"
}
Response:
{
"data": {
"pack": {
"id": "pack_abc123",
"name": "financial-analysis",
"version": "2.0.0",
"commands": ["analyze", "report", "compare"]
},
"message": "Pack installed successfully"
}
}
Uninstall Pack¶
Remove a pack and all its versions, commands, and access grants. Only the owning tenant's developer-mode users can delete a pack.
DELETE /api/v1/packs/{pack_id}
Authorization: Bearer {token}
Response (200):
{
"data": {
"message": "Pack uninstalled successfully"
}
}
Error Responses:
| Status | Condition |
|---|---|
| 403 | User is not in developer mode |
| 403 | Pack is not owned by user's tenant |
| 404 | Pack not found |
| 500 | Database error |
Side Effects:
- All pack versions, commands, and access grants are cascade-deleted
- Pack virtual environment and wheel files are removed from disk
- Worker command registries are notified to deregister the pack's commands
- Propagation to all workers completes within seconds via Celery task
Dashboards¶
Register Dashboard¶
Create a new dashboard in the registry. Only users in developer mode can create dashboards.
POST /api/v1/dashboards
Authorization: Bearer {token}
Content-Type: application/json
Request:
{
"scope": "@acme",
"name": "claims-dashboard",
"description": "Claims management dashboard",
"category": "business",
"visibility": "organization"
}
| Field | Type | Required | Description |
|---|---|---|---|
scope |
string | Yes | Organization scope with @ prefix (e.g., @acme). Stored with @; responses return scope without the prefix. |
name |
string | Yes | Dashboard name (kebab-case, 3-50 chars) |
description |
string | No | Short description (max 200 chars; see manifest spec) |
category |
string | No | Category: business, analytics, productivity, etc. |
visibility |
string | No | public, unlisted, organization, private (default: private) |
Response:
{
"data": {
"id": "d-abc123",
"scope": "acme",
"name": "claims-dashboard",
"slug": "claims-dashboard",
"visibility": "organization",
"created_at": "2026-03-13T10:00:00Z"
}
}
Error Responses:
| Code | Error | Description |
|---|---|---|
| 403 | Developer mode required |
Caller is not in developer mode |
| 403 | Scope mismatch |
Scope doesn't match caller's tenant or organization |
| 409 | Already exists |
Dashboard with this scope/name already registered |
List Dashboards¶
Returns dashboards visible to the current user. Filters by visibility and access grants. Excludes unlisted dashboards from list results — they are accessible only via direct slug lookup.
GET /api/v1/dashboards
Authorization: Bearer {token}
Response:
{
"data": [
{
"id": "d-abc123",
"slug": "claims-dashboard",
"name": "Claims Dashboard",
"description": "Claims management dashboard",
"version": "1.2.0",
"entryPoint": "/api/v1/dashboards/claims-dashboard/bundle/1.2.0/main.js",
"minSdkVersion": "1.0.0",
"scope": "acme",
"category": "business",
"visibility": "organization",
"packDependencies": [
{ "scope": "@acme", "name": "claims-processor", "version": ">=2.0.0" }
]
}
]
}
Visibility filtering: A dashboard is visible if it is public, owned by the caller's tenant, or has a DashboardAccessGrant for the caller's tenant (for organization visibility).
Get Dashboard by Slug¶
Returns a single dashboard's metadata and the active version's entry point URL. This is the primary endpoint Hub calls when a user navigates to /d/{slug}.
Returns 404 (not 403) for dashboards the caller cannot access, to prevent information leakage about the existence of private dashboards.
GET /api/v1/dashboards/{slug}
Authorization: Bearer {token}
Response:
{
"data": {
"id": "d-abc123",
"slug": "claims-dashboard",
"name": "Claims Dashboard",
"description": "Claims management dashboard",
"version": "1.2.0",
"entryPoint": "/api/v1/dashboards/claims-dashboard/bundle/1.2.0/main.js",
"minSdkVersion": "1.0.0",
"scope": "acme",
"category": "business",
"visibility": "organization",
"packDependencies": [
{ "scope": "@acme", "name": "claims-processor", "version": ">=2.0.0" }
]
}
}
The entryPoint is a fully-qualified URL path that Hub passes directly to import().
Upload Dashboard Version¶
Uploads a new dashboard version as a gzipped tarball. Uses multipart form data. Only the dashboard owner can upload versions.
POST /api/v1/dashboards/{dashboard_id}/versions
Authorization: Bearer {token}
Content-Type: multipart/form-data
Form fields:
| Field | Type | Description |
|---|---|---|
bundle |
file | Gzipped tarball (.tar.gz) containing main.js and optional assets |
version |
string | Semver version string (e.g., 1.2.0) |
Response:
{
"data": {
"id": "dv-xyz789",
"dashboard_id": "d-abc123",
"version": "1.2.0",
"entry_point": "main.js",
"min_sdk_version": "1.0.0",
"bundle_size_bytes": 867492,
"is_active": true,
"created_at": "2026-03-13T10:05:00Z"
}
}
Error Responses:
| Code | Error | Description |
|---|---|---|
| 400 | Entry point not found |
main.js missing from bundle |
| 400 | Invalid tarball format |
Cannot extract the uploaded file |
| 403 | Not owner |
Only dashboard owner can upload |
| 413 | Size limit exceeded |
Bundle exceeds 50 MB |
Processing: The backend extracts the tarball, validates the entry point and file inventory, stores files to {HUITZO_DASHBOARDS_DIR}/{dashboard_id}/{version}/, creates the version record, and deactivates previous versions — all in a single database transaction.
Serve Bundle Files¶
Serves individual files from a dashboard bundle. Called by the browser when Hub executes import(entryPointUrl).
GET /api/v1/dashboards/{slug}/bundle/{version}/{path:path}
Authorization: Bearer <jwt>
Response headers:
| Header | Value | Why |
|---|---|---|
Content-Type |
MIME type based on extension | Browser needs correct type for import() |
Cache-Control |
private, max-age=31536000, immutable |
Bundle files are immutable per version |
Error Responses:
| Code | Error | Description |
|---|---|---|
| 401 | Missing authorization |
No JWT provided |
| 404 | Dashboard not found |
Slug doesn't exist or caller lacks access |
| 404 | File not found |
Requested path doesn't exist in bundle |
Authentication: This endpoint uses standard JWT auth. The browser's native import() cannot send custom headers, so Hub's service worker intercepts bundle requests and injects the JWT. See Dashboard Loading Architecture for details.
Security: Path traversal is prevented by resolving the path within the bundle directory and rejecting paths that escape it.
Grant Dashboard Access¶
Grants organization-visibility dashboard access to specific tenants. Only the dashboard owner can grant access.
POST /api/v1/dashboards/{dashboard_id}/grant
Authorization: Bearer {token}
Content-Type: application/json
Request:
{
"tenant_ids": ["tenant-abc", "tenant-def"]
}
Response:
{
"data": {
"granted": 2,
"dashboard_id": "d-abc123"
}
}
Error Responses:
| Code | Error | Description |
|---|---|---|
| 400 | Wrong visibility |
Can only grant access to organization-visibility dashboards |
| 403 | Not owner |
Only dashboard owner can grant access |
| 404 | Not found |
Dashboard doesn't exist |
Delete Dashboard¶
Remove a dashboard and all its versions and access grants. Only the owning tenant's developer-mode users can delete a dashboard.
DELETE /api/v1/dashboards/{dashboard_id}
Authorization: Bearer {token}
Response (200):
{
"data": {
"message": "Dashboard deleted successfully"
}
}
Error Responses:
| Code | Error | Description |
|---|---|---|
| 403 | Developer mode required |
User is not in developer mode |
| 404 | Not found |
Dashboard does not exist or is not owned by user's tenant |
| 500 | Database error |
Internal database failure |
Side Effects:
- All dashboard versions and access grants are cascade-deleted
- Dashboard bundle files are removed from disk
- A
dashboard_deletedevent is emitted withdashboard_id,scope,name, andversions_removed
Dashboard Visibility Levels¶
Dashboard access control mirrors the Pack access control model with four visibility levels:
| Level | Discovery | Access | Use Case |
|---|---|---|---|
public |
Listed in Explore, searchable | No grant required | Open source, community dashboards |
unlisted |
Hidden from search and browse | Accessible via direct slug | Beta testing, invitation-only |
organization |
Visible to granted tenants | Requires DashboardAccessGrant |
Agency distribution, enterprise |
private |
Owner only | Owner tenant only | Internal tools, development |
Access control is enforced at every layer:
| Level | Enforcement |
|---|---|
| Discovery | GET /api/v1/dashboards filters by visibility + grants |
| Metadata | GET /api/v1/dashboards/{slug} returns 404 if caller lacks access |
| Bundle serving | GET /api/v1/dashboards/{slug}/bundle/... requires JWT and checks the same access rules |
Storage¶
Save Data¶
Save data to pack storage.
PUT /api/v1/storage/{key}
Authorization: Bearer {token}
Content-Type: application/json
Request:
{
"data": {
"title": "My Note",
"content": "Hello world"
},
"ttl": 86400
}
Response:
{
"data": {
"key": "notes:note_123",
"created": true,
"expires_at": "2025-01-23T10:30:00Z"
}
}
Get Data¶
GET /api/v1/storage/{key}
Authorization: Bearer {token}
Response:
{
"data": {
"key": "notes:note_123",
"value": {
"title": "My Note",
"content": "Hello world"
},
"created_at": "2025-01-22T10:30:00Z",
"expires_at": "2025-01-23T10:30:00Z"
}
}
Delete Data¶
DELETE /api/v1/storage/{key}
Authorization: Bearer {token}
Response:
{
"data": {
"deleted": true
}
}
List Keys¶
GET /api/v1/storage
Authorization: Bearer {token}
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
prefix |
string | - | Filter by key prefix |
limit |
integer | 100 | Max keys to return |
Response:
{
"data": {
"keys": [
"notes:note_123",
"notes:note_456",
"settings:preferences"
],
"total": 3
}
}
Users¶
Get Current User¶
GET /api/v1/auth/me
Authorization: Bearer {token}
Response:
{
"data": {
"id": "usr_abc123",
"email": "[email protected]",
"name": "John Doe",
"role": "admin",
"developerMode": true,
"subscriptionTier": "free",
"tenantSlug": "john-doe",
"createdAt": "2025-01-01T00:00:00Z"
}
}
Check Namespace¶
GET /api/v1/namespaces/{namespace}/check
Authorization: Bearer {token}
Checks whether a namespace is available for the current user. A namespace is "available" if the user owns it (either as their personal tenant slug or as a member of an organization with that slug).
Response (owned — personal):
{
"data": {
"available": true,
"owner": "personal"
}
}
Response (owned — organization):
{
"data": {
"available": true,
"owner": "organization"
}
}
Response (taken by someone else):
{
"data": {
"available": false,
"error": "This namespace is already in use"
}
}
Response (unclaimed — no tenant or org exists with this slug):
{
"data": {
"available": false,
"error": "Namespace not found. Create an organization with this slug first, or use your personal namespace."
}
}
Update Current User¶
PATCH /api/v1/users/me
Authorization: Bearer {token}
Content-Type: application/json
Request:
{
"name": "Jane Doe"
}
Health¶
Health Check¶
GET /health
Response:
{
"status": "healthy",
"version": "2.0.0",
"services": {
"database": "healthy",
"redis": "healthy",
"worker": "healthy"
},
"timestamp": "2025-01-22T10:30:00Z"
}
Access Codes¶
Generate Access Codes¶
Generate access codes in batch. Admin only.
POST /api/v1/access-codes
Authorization: Bearer {token}
Content-Type: application/json
Request:
{
"count": 10,
"tier": "pro",
"prefix": "HUITZO",
"cohort": "comp",
"max_uses": 1
}
tier and cohort are now mandatory inputs — alpha-era defaults were dropped in migration 022. Common values: tier is one of free/pro; cohort reflects the source of the grant (comp, partner, cross-promo, etc.).
Response:
{
"data": {
"codes": [
"HUITZO-A1B2-C3D4",
"HUITZO-E5F6-G7H8"
],
"count": 10,
"tier": "pro",
"cohort": "comp"
}
}
Validate Access Code¶
Check if an access code is valid. Public endpoint (no auth required).
POST /api/v1/access-codes/validate
Content-Type: application/json
Request:
{
"code": "HUITZO-A1B2-C3D4"
}
Response (Valid):
{
"data": {
"valid": true,
"tier": "developer_preview"
}
}
Response (Invalid):
{
"success": false,
"error": {
"message": "Invalid or expired access code"
}
}
List Access Codes¶
List all access codes with status. Admin only.
GET /api/v1/access-codes
Authorization: Bearer {token}
Response:
{
"data": [
{
"id": "uuid-abc123",
"code": "HUITZO-A1B2-C3D4",
"tier": "developer_preview",
"cohort": "founding-feb15",
"max_uses": 1,
"use_count": 1,
"is_active": true,
"redeemed_by": "uuid-user123",
"redeemed_at": "2026-02-15T10:00:00Z",
"created_at": "2026-02-11T09:00:00Z"
}
]
}
Deactivate Access Code¶
Deactivate an access code. Admin only.
DELETE /api/v1/access-codes/{code_id}
Authorization: Bearer {token}
Response:
{
"data": {
"id": "uuid-abc123",
"is_active": false
}
}
Registration¶
The registration surface is public — paid signups originate without an authenticated session. The endpoints listed here will be implemented in PR-PR02 / PR-39 of the paid-registration roadmap. They are documented now so PR-PR02's Implements: headers have a stable anchor.
Start Registration Checkout¶
Open a Stripe Checkout session for the paid signup flow. Public, IP-rate-limited (5 requests / 60 seconds per IP). Email collection happens on the Stripe-hosted page, not in this payload.
POST /api/v1/registration/checkout
Content-Type: application/json
Request:
{
"ref": "acme"
}
ref is sanitized server-side to alphanumeric + dash, length ≤ 64. Empty / null is allowed (no referral attribution).
Response:
{
"data": {
"checkout_url": "https://checkout.stripe.com/c/pay/...",
"session_id": "cs_test_..."
}
}
Get Registration Session Status¶
Public, polled by the post-payment /register?session={id} page until the webhook lands. Lookup uses the partial unique index on access_codes.metadata->>'stripe_session_id' from migration 022.
GET /api/v1/registration/session/{session_id}
Response (pending — webhook has not arrived yet):
{
"data": {
"status": "pending"
}
}
Response (ready — code minted, email queued):
{
"data": {
"status": "ready",
"code": "HUITZO-A1B2-C3D4",
"email": "[email protected]"
}
}
Registration Webhook¶
Stripe-only. Public, signature-verified with HUITZO_STRIPE_WEBHOOK_SECRET_REGISTRATION (D3). Idempotency is enforced by inserting event.id into stripe_events_processed (D13) — duplicate deliveries short-circuit.
POST /api/v1/registration/webhook
Stripe-Signature: t=...,v1=...
Content-Type: application/json
Handled events: checkout.session.completed, checkout.session.expired, charge.dispute.created. The handler must validate against exactly one signing secret — multi-secret fallback is forbidden by D3.
Billing¶
Get Billing Status¶
Get current subscription status for the authenticated user.
GET /api/v1/billing/status
Authorization: Bearer {token}
Response:
{
"data": {
"subscription_tier": "developer_preview",
"stripe_customer_id": null,
"cohort": "founding-feb15"
}
}
List Plans¶
List available subscription plans. Public endpoint.
GET /api/v1/billing/plans
Response:
{
"data": [
{
"id": "free",
"name": "Free",
"price": 0,
"interval": null,
"limits": { "requests_per_minute": 10, "commands_per_hour": 5 },
"features": ["Basic access", "Community support"]
},
{
"id": "developer_preview",
"name": "Developer Preview",
"price": 0,
"interval": null,
"limits": { "requests_per_minute": 60, "commands_per_hour": -1 },
"features": ["Unlimited commands", "Early access", "Founding member benefits"]
},
{
"id": "pro",
"name": "Pro",
"price": 5000,
"interval": "month",
"limits": { "requests_per_minute": 300, "commands_per_hour": -1 },
"features": ["Unlimited commands", "Priority support", "Advanced analytics", "Custom integrations"]
}
]
}
Create Checkout Session¶
Create a Stripe Checkout session for Pro subscription. Redirects user to Stripe-hosted payment page.
POST /api/v1/billing/checkout
Authorization: Bearer {token}
Content-Type: application/json
Request:
{
"success_url": "https://hub.huitzo.com/billing?success=true",
"cancel_url": "https://hub.huitzo.com/billing?cancelled=true"
}
Response:
{
"data": {
"checkout_url": "https://checkout.stripe.com/c/pay/cs_test_..."
}
}
Create Portal Session¶
Create a Stripe Customer Portal session for managing billing.
POST /api/v1/billing/portal
Authorization: Bearer {token}
Content-Type: application/json
Request:
{
"return_url": "https://hub.huitzo.com/billing"
}
Response:
{
"data": {
"portal_url": "https://billing.stripe.com/p/session/..."
}
}
Stripe Webhook¶
Handle Stripe webhook events. Uses Stripe signature verification (no JWT auth).
POST /api/v1/billing/webhook
Stripe-Signature: t=...,v1=...
Handles events: checkout.session.completed, customer.subscription.updated, customer.subscription.deleted, invoice.payment_succeeded, invoice.payment_failed.
Response:
{
"data": {
"received": true
}
}
Metrics¶
All metrics endpoints require admin role (owner or admin).
Onboarding Funnel¶
GET /api/v1/metrics/onboarding?since=2026-02-15T00:00:00
Authorization: Bearer {token}
Response:
{
"data": {
"funnel": [
{ "step": "signup", "count": 200, "conversion_pct": 100.0 },
{ "step": "access_code_redeemed", "count": 195, "conversion_pct": 97.5 },
{ "step": "developer_activated", "count": 150, "conversion_pct": 76.9 },
{ "step": "pack_created", "count": 80, "conversion_pct": 53.3 },
{ "step": "command_executed", "count": 60, "conversion_pct": 75.0 }
]
}
}
Revenue Metrics¶
GET /api/v1/metrics/revenue
Authorization: Bearer {token}
Response:
{
"data": {
"mrr_cents": 250000,
"active_pro_subscribers": 50,
"recent_events": [
{
"type": "payment_succeeded",
"data": { "amount": 5000 },
"created_at": "2026-02-20T10:00:00Z"
}
]
}
}
Usage Metrics¶
GET /api/v1/metrics/usage?since=2026-02-15T00:00:00
Authorization: Bearer {token}
Response:
{
"data": {
"commands_executed": 1500,
"active_users": 120,
"error_count": 15,
"avg_command_duration_ms": 342.5
}
}
User Journeys¶
Per-user event timelines for the admin dashboard.
GET /api/v1/metrics/user-journeys
Authorization: Bearer {token}
Response:
{
"data": [
{
"user_id": "uuid-abc",
"email": "[email protected]",
"cohort": "founding-feb15",
"subscription_tier": "developer_preview",
"created_at": "2026-02-15T10:00:00Z",
"events_count": 25,
"time_to_first_command_ms": 180000,
"last_active": "2026-02-20T15:30:00Z",
"events": [
{ "type": "signup", "created_at": "2026-02-15T10:00:00Z", "duration_ms": null },
{ "type": "command_executed", "created_at": "2026-02-15T10:03:00Z", "duration_ms": 450 }
]
}
]
}
Export Metrics¶
Bulk export billing events for analysis.
GET /api/v1/metrics/export?format=csv&since=2026-02-15T00:00:00
Authorization: Bearer {token}
Formats: csv (default), json
CSV response returns a downloadable file with columns: id, tenant_id, user_id, event_type, event_data, duration_ms, created_at.
Admin¶
All admin endpoints require an admin or owner JWT and write one row to admin_audit_log per request. Read paths use the app.tenant_id='__admin__' RLS sentinel; cross-tenant writes (revoke sessions, change tier) re-open a per-target-tenant session so standard tenant isolation policies apply.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/v1/admin/users |
List users; filters: tier, referral, created_after. Cursor or offset pagination, limit 1-200 (default 50). |
| GET | /api/v1/admin/users/{id} |
Detail view: user record, recent billing events, redeemed access code (HMAC sidecar only — never raw), live Stripe subscription status (60s cached). |
| POST | /api/v1/admin/users/{id}/access-codes |
Mint one comp access code. Body: {tier, expires_at?, max_uses?, kind: 'comp'}. Response carries the generated code literal exactly once; audit log carries only the HMAC sidecar. |
| POST | /api/v1/admin/users/{id}/sessions/revoke |
Delete every refresh-session row for the target user. |
| POST | /api/v1/admin/users/{id}/tier |
Manual tier change. Body: {tier, reason}. Best-effort SendGrid notification (failure does not roll back). |
| GET | /api/v1/admin/access-codes |
List with filters: kind, redemption_status (redeemed/unredeemed), referral. Raw code literals are NOT included. |
| GET | /api/v1/admin/metrics/funnel |
Daily/weekly aggregate of checkout_started → checkout_completed → registered → first_command. Single GROUP BY query; p95 < 500 ms with 10K user fixtures. |
Non-admin requests (role NOT IN ('admin','owner')) receive 403 before any audit row is written. Unauthenticated requests receive 401.
PII handling (D11 / #G15): plaintext access codes, full stripe_*_id, and full emails are never persisted in admin_audit_log — the writer applies HMAC-SHA256 sidecars or last-4-of-localpart redaction before INSERT. The stripe_customer_id field is replaced by a stripe_customer_id_present boolean in every response body.
MCP Endpoint¶
A Streamable-HTTP Model Context Protocol server that exposes the caller's tenant-visible pack commands as MCP tools. Designed for connection from Claude.ai, Claude Desktop, Cursor, VS Code, and any other Streamable-HTTP MCP client. The full user-facing setup walkthrough is in guides/connect-claude-mcp.md.
Endpoint¶
POST /mcp
Authorization: Bearer sk-huitzo-...
Content-Type: application/json
The path is /mcp (not /api/v1/mcp) so the URL pasted into a connector config stays short.
Authentication¶
API key only. The sk-huitzo-* key must carry the commands:execute scope. JWT and cookie credentials are rejected by design — connector configs sit in client UIs for months and need long-lived, scoped, revocable credentials.
| Status | Cause |
|---|---|
| 401 — Authentication required | Missing Authorization header |
| 401 — requires API-key authentication | Sent a JWT or cookie instead of an API key |
| 401 — Invalid or revoked API key | Key revoked or never existed |
403 — missing commands:execute scope |
API key authenticated but lacks the required scope. Generate a new key with the commands:execute scope. |
Supported MCP methods¶
| JSON-RPC method | Behavior |
|---|---|
initialize |
Standard MCP handshake — declares server capabilities (tools only) |
tools/list |
Returns every command visible to the authenticated tenant |
tools/call |
Invokes a command via the same ExecutionRouter used by POST /api/v1/commands/{namespace}. No internal HTTP round-trip — the MCP handler resolves the command, builds the Context, and calls the executor directly under the same RLS tenant scope. Counted against the per-tier command rate-limit bucket. |
ping |
Health probe |
Tool naming¶
Huitzo namespaces (@scope/pack/command) are mapped deterministically to MCP-legal tool names:
@scope/pack/command ↔ scope__pack__command
Round-trip: split on __, prepend @. Pack names are validated at publish time against [a-z0-9-]+, so the mapping is lossless by construction.
Visibility & isolation¶
tools/list returns the same set of commands as GET /api/v1/commands — public packs, the caller's private packs, and packs shared with the caller's tenant via organization grants. Unlisted packs are excluded. Tenant isolation is enforced by PostgreSQL RLS exactly as on the REST endpoint; Claude.ai cannot see another tenant's private commands.
Result shape¶
tools/call always returns a single text content block whose body is JSON. Three shapes are possible:
1. Synchronous success (fast-queue commands — the common case):
{ "result": <command_result>, "correlation_id": "..." }
2. Asynchronous dispatch (medium- or long-queue commands):
{
"pending": true,
"task_id": "celery-...",
"queue": "medium",
"namespace": "@scope/pack/command",
"correlation_id": "...",
"message": "Command queued on the medium pool. Poll GET /api/v1/tasks/{task_id} for the result."
}
The MCP handler does not block on async commands — the client (or the conversational model in front of it) is responsible for polling GET /api/v1/tasks/{task_id} to retrieve the eventual result. Most pack commands run on the fast queue; this branch is rare.
3. Error (typed envelope; type is one of validation, command, timeout, internal):
{ "error": "<user-facing message or generic label>", "type": "internal", "correlation_id": "..." }
The MCP server intentionally never surfaces str(exc) for unknown exception types — only ValidationError and CommandError (designed to be user-facing) carry their original message. Operators correlate via the correlation_id.
Telemetry¶
Submit CLI Telemetry¶
Lightweight telemetry ingestion for CLI usage data. Public endpoint (opt-in, anonymous).
POST /api/v1/telemetry
Content-Type: application/json
Request:
{
"command": "build",
"duration_ms": 1200,
"success": true,
"python_version": "3.14.0",
"os": "linux",
"cli_version": "0.1.0"
}
Response:
{
"data": {
"received": true
}
}
Rate Limiting¶
API requests are rate limited per user based on subscription tier.
| Tier | Requests/min | Commands/hour |
|---|---|---|
free |
10 | 5 |
developer_preview |
60 | Unlimited |
pro |
300 | Unlimited |
enterprise |
1000 | Unlimited |
Unlimited is represented as -1 in the plans API.
Rate Limit Headers:
X-RateLimit-Remaining: 45
Retry-After: 60
| Header | Description |
|---|---|
X-RateLimit-Remaining |
Requests remaining in current window |
Retry-After |
Seconds until limit resets (only on 429 responses) |
Rate Limit Response (429):
{
"success": false,
"error": {
"message": "Rate limit exceeded"
}
}
Pagination¶
List endpoints support pagination:
GET /api/v1/commands?page=2&limit=50
Pagination Response:
{
"data": { ... },
"pagination": {
"total": 150,
"page": 2,
"pages": 3,
"limit": 50,
"has_next": true,
"has_prev": true
}
}
Webhooks¶
Configure webhooks to receive notifications.
Supported Events¶
| Event | Description |
|---|---|
command.executed |
Command execution completed |
task.completed |
Async task completed |
task.failed |
Async task failed |
pack.installed |
Pack installed |
pack.uninstalled |
Pack uninstalled |
Webhook Payload¶
{
"id": "evt_abc123",
"type": "command.executed",
"timestamp": "2025-01-22T10:30:00Z",
"data": {
"command": "financial.analyze",
"tenant_id": "ten_xyz789",
"user_id": "usr_abc123",
"duration_ms": 1234
}
}
SDKs¶
Official SDKs are available:
- Python:
pip install huitzo-sdk - JavaScript:
npm install @huitzo/sdk
See SDK Documentation for usage.
OpenAPI Specification¶
Full OpenAPI 3.0 specification available at:
https://huitzo.ai/openapi.json
Interactive documentation:
https://huitzo.ai/docs