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
  }
}

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_deleted event is emitted with dashboard_id, scope, name, and versions_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