HOT API
The Hot API provides programmatic access to manage projects, builds, runs, events, and more.
Base URL
https://api.hot.dev/v1
For local development:
http://localhost:4681/v1
Authentication
All API requests (except health checks) require a bearer token in the Authorization header:
curl https://api.hot.dev/v1/projects \
-H "Authorization: Bearer <token>"
Hot supports three credential types — API keys, service keys, and sessions — all used the same way. All credentials are scoped to an environment, and resources are automatically filtered to that environment's context.
See the Authentication documentation for full details on credential types, the permissions model, and the permissions builder.
Permissions Model
API keys, service keys, and sessions share a granular permission system. Permissions are a JSON map of resource URNs to action arrays:
{
"mcp:weather/get-forecast": ["execute"],
"stream:*": ["read"],
"event:user:*": ["create", "read"]
}
Resource URN format: type:path
typeis the resource categorypathis the resource identifier (*for wildcard)
Resource types and valid actions:
| Resource Type | Valid Actions | Description |
|---|---|---|
mcp | execute | MCP tool invocation (e.g., mcp:weather/get-forecast) |
webhook | execute | Webhook endpoint access (e.g., webhook:payments/*) |
stream | read | Stream subscription (e.g., stream:* or stream:<id>) |
event | create, read | Event publishing and reading |
run | read | Run inspection |
call | create, read | Function call invocation |
project | create, read, update, delete | Project management |
build | create, read, execute | Build management and deployment |
context | create, read, update, delete | Context variable management |
key | create, read, update, delete | API key management |
session | create, read, delete | Session management |
env | read | Environment information |
The wildcard action * grants all valid actions for that resource type. The universal wildcard *:* with ["*"] grants unrestricted access.
Validation Rules:
Permissions are validated when creating or updating API keys, sessions, and service keys. The following rules apply:
| Rule | Example (rejected) | Error |
|---|---|---|
Resource must use type:path format | "no-colon-here" | Invalid resource |
| Resource key must not be empty | "" | Invalid resource |
| Type must not be empty | ":path" | Invalid resource |
| Path must not be empty | "mcp:" | Invalid resource |
Bare * is not valid — use *:* | "*" | Invalid resource |
* type only allows * path | "*:foo" | Invalid resource |
| Type must be alphanumeric/hyphens | "mcp!:test" | Invalid resource |
| Actions must not be empty | {"mcp:*": []} | Empty action list |
| Actions are lowercase only | "Read", "CREATE" | Invalid action |
Only valid actions: create, read, update, delete, execute, * | "destroy" | Invalid action |
| Action must be valid for the resource type | "mcp:*": ["create"] | Action not valid for resource |
Sessions and service keys must also be a subset of the parent API key's permissions — you cannot escalate permissions beyond what the issuing key allows.
Rate Limiting
API requests are rate limited per organization. Limits are based on your subscription plan:
| Plan | Requests per Second |
|---|---|
| Starter | 20 RPS |
| Pro | 100 RPS |
| Enterprise / Self-hosted | Unlimited |
When the limit is exceeded, the API returns 429 Too Many Requests with a Retry-After header indicating how many seconds to wait before retrying.
Response Format
All responses use a consistent envelope format.
Success Response (Single Item)
{
"data": {
"project_id": "550e8400-e29b-41d4-a716-446655440000",
"name": "my-project"
},
"meta": {
"request_id": "123e4567-e89b-12d3-a456-426614174000",
"timestamp": "2024-01-15T10:30:00Z"
}
}
Success Response (List)
{
"data": [...],
"pagination": {
"total": 42,
"limit": 20,
"offset": 0,
"has_more": true
},
"meta": {
"request_id": "123e4567-e89b-12d3-a456-426614174000",
"timestamp": "2024-01-15T10:30:00Z"
}
}
Error Response
{
"error": {
"code": "not_found",
"message": "Project not found",
"request_id": "123e4567-e89b-12d3-a456-426614174000"
}
}
Pagination
List endpoints support pagination via query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | int | 20 | Maximum results to return |
offset | int | 0 | Number of results to skip |
Endpoints
Health & Status
Get API Status
GET /status
Returns API server health information. No authentication required.
Response:
{
"status": "ok",
"service": "hot.dev api server",
"version": "1.0.0",
"git_sha": "abc1234",
"start_time": "2026-01-15T10:00:00Z"
}
Projects
List Projects
GET /v1/projects
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
limit | int | Max results (default: 20) |
offset | int | Pagination offset |
Response:
{
"data": [
{
"project_id": "550e8400-e29b-41d4-a716-446655440000",
"env_id": "660e8400-e29b-41d4-a716-446655440000",
"name": "my-project",
"active": true,
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}
],
"pagination": {...},
"meta": {...}
}
Create Project
POST /v1/projects
Request Body:
{
"name": "my-project"
}
Response: 201 Created with project data.
Get Project
GET /v1/projects/{project_id_or_slug}
Supports both UUID and project name (slug) in the URL.
Update Project
PATCH /v1/projects/{project_id_or_slug}
Request Body:
{
"name": "new-project-name"
}
Delete Project
DELETE /v1/projects/{project_id_or_slug}
Response: 204 No Content
Builds
List Builds (All in Environment)
GET /v1/builds
Lists all builds across all projects in the environment.
Response includes project_name for each build.
List Builds (By Project)
GET /v1/projects/{project_id_or_slug}/builds
Get Build
GET /v1/projects/{project_id_or_slug}/builds/{build_id}
Response:
{
"data": {
"build_id": "550e8400-e29b-41d4-a716-446655440000",
"project_id": "660e8400-e29b-41d4-a716-446655440000",
"hash": "abc123def456",
"size": 102400,
"build_type": "bundle",
"deployed": false,
"active": true,
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z",
"storage_path": "s3://builds/...",
"storage_backend": "s3"
},
"meta": {...}
}
Get Deployed Build
GET /v1/projects/{project_id_or_slug}/builds/deployed
Returns the currently deployed build for the project, or 404 if none.
Get Live Build
GET /v1/projects/{project_id_or_slug}/builds/live
Returns the live (development) build for the project, or 404 if none.
Upload Build
POST /v1/projects/{project_id_or_slug}/builds
Content-Type: multipart/form-data
Form Fields:
| Field | Required | Description |
|---|---|---|
file | Yes | The build zip file |
hash | Yes | SHA hash of the build for validation |
build_id | No | Optional UUID; if provided, enables idempotent uploads |
Examples:
curl
curl -X POST 'https://api.hot.dev/v1/projects/my-project/builds' \
-H "Authorization: Bearer $HOT_API_KEY" \
-F "file=@build.hot.zip" \
-F "hash=$(sha256sum build.hot.zip | cut -d' ' -f1)"
JavaScript
const fs = require('fs');
const crypto = require('crypto');
const FormData = require('form-data');
const file = fs.readFileSync('build.hot.zip');
const hash = crypto.createHash('sha256').update(file).digest('hex');
const form = new FormData();
form.append('file', file, 'build.hot.zip');
form.append('hash', hash);
const response = await fetch(`${BASE_URL}/projects/my-project/builds`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${HOT_API_KEY}` },
body: form
});
Python
import hashlib
with open('build.hot.zip', 'rb') as f:
file_hash = hashlib.sha256(f.read()).hexdigest()
response = requests.post(
f'{BASE_URL}/projects/my-project/builds',
headers={'Authorization': f'Bearer {HOT_API_KEY}'},
files={'file': open('build.hot.zip', 'rb')},
data={'hash': file_hash}
)
Response: 201 Created
{
"data": {
"build_id": "550e8400-e29b-41d4-a716-446655440000",
"project_id": "660e8400-e29b-41d4-a716-446655440000",
"hash": "abc123def456",
"size": 102400,
"storage_path": "s3://builds/...",
"storage_backend": "s3",
"created_at": "2024-01-15T10:30:00Z"
},
"meta": {...}
}
If the build_id already exists, returns 200 OK with header X-Build-Exists: true.
Download Build
GET /v1/projects/{project_id_or_slug}/builds/{build_id}/download
Returns the build as a zip file with Content-Type: application/zip.
Deploy Build
POST /v1/projects/{project_id_or_slug}/builds/{build_id}/deploy
Marks the build as deployed and queues it for worker processing.
Context Variables (Secrets)
Context variables are encrypted secrets stored per-project. Values are encrypted at rest using AES-256-GCM.
Security: Values are never returned via API—only metadata (key, description, timestamps).
List Context Variables
GET /v1/projects/{project_id_or_slug}/context
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
limit | int | Max results (default: 20) |
offset | int | Pagination offset |
Response:
{
"data": [
{
"key": "DATABASE_URL",
"description": "Production database connection",
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}
],
"pagination": {...},
"meta": {...}
}
Create Context Variable
POST /v1/projects/{project_id_or_slug}/context
Request Body:
{
"key": "DATABASE_URL",
"value": "postgres://user:pass@host/db",
"description": "Production database connection"
}
Response: 201 Created (value is not returned).
Update Context Variable
PUT /v1/projects/{project_id_or_slug}/context/{key}
Request Body:
{
"value": "postgres://user:newpass@host/db",
"description": "Updated description"
}
Delete Context Variable
DELETE /v1/projects/{project_id_or_slug}/context/{key}
Response: 204 No Content
Events
Publish Event
POST /v1/events
Publishes an event that can trigger event handlers.
event_type is an arbitrary string chosen by your application (for example user:created).
Hot does not enforce a specific naming pattern, but :-separated names are the recommended convention.
For comparison:
- Hot language
send(...)usestypeanddata - HTTP API
POST /v1/eventsusesevent_typeandevent_data
Request Body:
{
"event_type": "user.signup",
"event_data": {
"user_id": "123",
"email": "alice@example.com"
}
}
Response: 201 Created
{
"data": {
"event_id": "550e8400-e29b-41d4-a716-446655440000",
"env_id": "660e8400-e29b-41d4-a716-446655440000",
"stream_id": "770e8400-e29b-41d4-a716-446655440000",
"event_type": "user.signup",
"event_data": {...},
"event_time": "2024-01-15T10:30:00Z",
"created_at": "2024-01-15T10:30:00Z"
},
"meta": {...}
}
List Events
GET /v1/events
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
limit | int | Max results (default: 20) |
offset | int | Pagination offset |
Get Event
GET /v1/events/{event_id}
Get Runs for Event
GET /v1/events/{event_id}/runs
Returns all runs triggered by this event.
Runs
List Runs
GET /v1/runs
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
limit | int | Max results (default: 20) |
offset | int | Pagination offset |
status | string | Filter: running, succeeded, failed, cancelled |
type | string | Filter: call, event, schedule, run, eval, repl |
time_range | string | ISO 8601 duration: P7D, P30D, etc. |
Response:
{
"data": [
{
"run_id": "550e8400-e29b-41d4-a716-446655440000",
"env_id": "660e8400-e29b-41d4-a716-446655440000",
"stream_id": "770e8400-e29b-41d4-a716-446655440000",
"build_id": "880e8400-e29b-41d4-a716-446655440000",
"run_type": "event",
"status": "succeeded",
"start_time": "2024-01-15T10:30:00Z",
"stop_time": "2024-01-15T10:30:45Z",
"origin_run_id": null,
"event_id": "990e8400-e29b-41d4-a716-446655440000",
"result": {...},
"project_id": "aa0e8400-e29b-41d4-a716-446655440000",
"project_name": "my-project"
}
],
"pagination": {...},
"meta": {...}
}
Get Run
GET /v1/runs/{run_id}
Get Run Statistics
GET /v1/runs/stats
Response:
{
"data": {
"total_runs": 1234,
"running": 5,
"succeeded": 1200,
"failed": 25,
"cancelled": 4
},
"meta": {...}
}
Event Handlers
Event handlers are registered in your Hot code and loaded when builds are uploaded.
List Event Handlers
GET /v1/projects/{project_id_or_slug}/event-handlers
Returns event handlers from the project's deployed build.
Response:
{
"data": [
{
"event_handler_id": "550e8400-e29b-41d4-a716-446655440000",
"build_id": "660e8400-e29b-41d4-a716-446655440000",
"event_type": "user.signup",
"ns": "::myapp::handlers",
"var": "on-user-signup"
}
],
"pagination": {...},
"meta": {...}
}
Schedules
Schedules are cron-based triggers defined in your Hot code.
List Schedules
GET /v1/projects/{project_id_or_slug}/schedules
Returns schedules from the project's deployed build.
Response:
{
"data": [
{
"schedule_id": "550e8400-e29b-41d4-a716-446655440000",
"build_id": "660e8400-e29b-41d4-a716-446655440000",
"cron": "0 0 * * *",
"ns": "::myapp::tasks",
"var": "daily-cleanup"
}
],
"pagination": {...},
"meta": {...}
}
Environment
Get Environment Info
GET /v1/env
Response:
{
"data": {
"env_id": "550e8400-e29b-41d4-a716-446655440000",
"org_id": "660e8400-e29b-41d4-a716-446655440000",
"name": "production",
"active": true
},
"meta": {...}
}
Subscribe to Environment Events (SSE)
GET /v1/env/subscribe
Subscribe to real-time events for the entire environment via Server-Sent Events (SSE). This endpoint streams all run, event, and stream activity for the environment associated with your API key.
Response: text/event-stream
SSE Event Types:
| Event | Description |
|---|---|
run:start | A new run has started |
run:stop | A run completed successfully |
run:fail | A run failed |
run:cancel | A run was cancelled |
event:created | A new event was created |
event:handled | An event was handled |
stream:created | A new stream was created |
Example Events:
event: run:start
data: {"run_id":"550e8400-...","stream_id":"660e8400-...","run_type":"event"}
event: run:stop
data: {"run_id":"550e8400-...","stream_id":"660e8400-..."}
event: event:created
data: {"event_id":"770e8400-...","stream_id":"880e8400-...","event_type":"user.signup"}
Examples:
curl
curl -N 'https://api.hot.dev/v1/env/subscribe' \
-H "Authorization: Bearer $HOT_API_KEY"
Note:
-Ndisables buffering for real-time streaming output.
JavaScript
const eventSource = new EventSource('https://api.hot.dev/v1/env/subscribe', {
headers: { 'Authorization': `Bearer ${HOT_API_KEY}` }
});
eventSource.addEventListener('run:stop', (e) => {
const data = JSON.parse(e.data);
console.log('Run stopped:', data.run_id);
});
eventSource.addEventListener('event:created', (e) => {
const data = JSON.parse(e.data);
console.log('Event created:', data.event_type);
});
Python
import requests
import json
response = requests.get(
'https://api.hot.dev/v1/env/subscribe',
headers={'Authorization': f'Bearer {HOT_API_KEY}'},
stream=True
)
for line in response.iter_lines():
if line:
line = line.decode('utf-8')
if line.startswith('data: '):
event = json.loads(line[6:])
print(f"Event: {event}")
Notes:
- The stream automatically times out after 5 minutes. Reconnect to continue receiving events.
- Events are scoped to the environment associated with your API key.
- This endpoint requires pub/sub to be configured on the server.
Organization
Get Usage & Limits
GET /v1/org/usage
Returns current usage statistics, plan limits, and usage percentages for the organization.
Response:
{
"data": {
"org_id": "660e8400-e29b-41d4-a716-446655440000",
"usage": {
"runs_this_period": 1250,
"file_storage_bytes": 52428800,
"team_members": 5,
"call_storage_bytes": 104857600,
"call_count": 15000
},
"limits": {
"runs_per_month": 10000,
"storage_bytes": 1073741824,
"team_members": 10,
"call_retention_days": 30,
"call_storage_bytes": 5368709120
},
"usage_percent": {
"runs": 12.5,
"file_storage": 4.9,
"team_members": 50.0,
"call_storage": 1.95,
"has_warning": false
},
"plan": {
"name": "Pro",
"period_start": "2024-01-01T00:00:00Z",
"period_end": "2024-02-01T00:00:00Z"
}
},
"meta": {...}
}
Fields:
| Field | Description |
|---|---|
usage | Current usage in the billing period |
limits | Plan limits (-1 = unlimited) |
usage_percent | Usage as percentage of limits (can exceed 100 if over limit) |
usage_percent.has_warning | True if any usage exceeds 90% |
plan.period_start | Start of current billing period |
plan.period_end | End of current billing period |
For self-hosted/local deployments without a subscription, all limits are unlimited (-1).
Streams (Server-Sent Events)
Streams provide real-time updates for run execution via Server-Sent Events (SSE). A stream groups related events and runs together, allowing you to track the full lifecycle of an operation.
Subscribe to Stream
GET /v1/streams/{stream_id}/subscribe
Subscribe to an existing stream to receive real-time updates.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
project | string | Optional project filter |
Response: text/event-stream
SSE Event Types:
| Event | Description |
|---|---|
run:start | A new run has started |
run:stop | A run completed successfully |
run:fail | A run failed |
run:cancel | A run was cancelled |
stream:data | Real-time data from the run (e.g., AI tokens) |
stream:complete | Stream timed out (5 minute default) |
Example Event:
event: run:start
data: {"type":"run:start","run":{"run_id":"...","status":"running",...}}
event: stream:data
data: {"type":"stream:data","run_id":"...","data_type":"ai:delta","payload":{"text":"Hello"}}
event: run:stop
data: {"type":"run:stop","run":{"run_id":"...","status":"succeeded","result":"Hello world"}}
Subscribe with Event (Atomic)
POST /v1/streams/subscribe-with-event
Accept: text/event-stream
Recommended for streaming use cases. This endpoint atomically subscribes to a stream AND publishes an event in a single request, eliminating race conditions where events might be missed.
Request Body:
{
"event_type": "chat:message",
"event_data": {
"message": "Hello, world!",
"history": []
},
"stream_id": "optional-existing-stream-uuid"
}
| Field | Required | Description |
|---|---|---|
event_type | Yes | The event type to publish |
event_data | Yes | Event payload (any JSON) |
stream_id | No | Continue an existing stream; if omitted, creates a new stream |
Response: text/event-stream
The first event is always event:published confirming the event was queued:
event: event:published
data: {"type":"event:published","event_id":"...","stream_id":"...","event_type":"chat:message"}
event: run:start
data: {"type":"run:start","run":{...}}
event: stream:data
data: {"type":"stream:data","run_id":"...","data_type":"ai:delta","payload":{"text":"Hello"}}
event: run:stop
data: {"type":"run:stop","run":{...}}
Examples:
curl
curl -N -X POST 'https://api.hot.dev/v1/streams/subscribe-with-event' \
-H "Authorization: Bearer $HOT_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{"event_type": "chat:message", "event_data": {"message": "Hello!"}}'
Note:
-Ndisables buffering for real-time streaming output.
JavaScript
const response = await fetch('https://api.hot.dev/v1/streams/subscribe-with-event', {
method: 'POST',
headers: {
'Authorization': `Bearer ${HOT_API_KEY}`,
'Content-Type': 'application/json',
'Accept': 'text/event-stream',
},
body: JSON.stringify({
event_type: 'chat:message',
event_data: { message: 'Hello!', history: [] }
})
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
// Parse SSE events from chunk
for (const line of chunk.split('\n')) {
if (line.startsWith('data: ')) {
const event = JSON.parse(line.slice(6));
console.log(event.type, event);
}
}
}
Python
import requests
import json
response = requests.post(
'https://api.hot.dev/v1/streams/subscribe-with-event',
headers={
'Authorization': f'Bearer {HOT_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'text/event-stream',
},
json={
'event_type': 'chat:message',
'event_data': {'message': 'Hello!', 'history': []}
},
stream=True # Required for SSE
)
for line in response.iter_lines():
if line:
line = line.decode('utf-8')
if line.startswith('data: '):
event = json.loads(line[6:])
print(event['type'], event)
Sessions
Sessions are short-lived, permission-scoped tokens for ephemeral access. Only API keys can create sessions.
Create Session
POST /v1/sessions
Request Body:
{
"permissions": {
"stream:*": ["read"],
"event:user:*": ["create"]
},
"metadata": {
"user_id": "end-user-123",
"purpose": "stream-subscription"
},
"expires_in": 3600
}
| Field | Required | Description |
|---|---|---|
permissions | Yes | Permission map (resource URN → action array). Must be a subset of the parent API key's permissions. |
metadata | No | Arbitrary JSON metadata (user ID, purpose, etc.) |
expires_in | No | TTL in seconds (default: 3600, max: 86400) |
Response: 201 Created
{
"data": {
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"token": "s_0193a7b212347def8abc123456789012_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
"permissions": {"stream:*": ["read"], "event:user:*": ["create"]},
"metadata": {"user_id": "end-user-123"},
"expires_at": "2026-01-15T11:30:00Z",
"created_at": "2026-01-15T10:30:00Z"
},
"meta": {...}
}
Important: The
tokenfield is only returned at creation time. Store it securely — it cannot be retrieved later.
Errors:
| Code | Status | Cause |
|---|---|---|
forbidden | 403 | Non-API-key credential attempted to create a session |
permission_escalation | 403 | Requested permissions exceed parent API key permissions |
session_limit_exceeded | 429 | Maximum active sessions (1000) reached for this API key |
List Sessions
GET /v1/sessions
Lists active (non-expired, non-revoked) sessions for the authenticated API key.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
limit | int | Max results (default: 20) |
offset | int | Pagination offset |
Revoke Session
DELETE /v1/sessions/{session_id}
Revokes a specific session. The session must belong to the authenticated API key.
Response: 204 No Content
Revoke All Sessions
DELETE /v1/sessions
Revokes all active sessions for the authenticated API key.
Response:
{
"data": {
"revoked_count": 5
},
"meta": {...}
}
Service Keys
Service keys are long-lived, permission-scoped credentials you issue to your customers or external systems for access to MCP tools, webhooks, and other API resources. Only API keys can create service keys.
Create Service Key
POST /v1/service-keys
Request Body:
{
"name": "Acme Corp Production Key",
"description": "MCP and stream access for Acme Corp",
"permissions": {
"mcp:weather/*": ["execute"],
"stream:*": ["read"]
},
"metadata": {
"customer_id": "acme-123"
},
"expires_in": null
}
| Field | Required | Description |
|---|---|---|
name | No | Human-readable name |
description | No | Description of the key's purpose |
permissions | Yes | Permission map. Must be a subset of the parent API key's permissions. |
metadata | No | Arbitrary JSON metadata (encrypted at rest, available at runtime via req.auth.service-key.meta) |
expires_in | No | TTL in seconds (null or omitted = never expires) |
Response: 201 Created
{
"data": {
"service_key_id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Acme Corp Production Key",
"description": "MCP and stream access for Acme Corp",
"token": "0193a7b212347def8abc123456789012_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
"permissions": {"mcp:weather/*": ["execute"], "stream:*": ["read"]},
"metadata": {"customer_id": "acme-123"},
"created_at": "2026-01-15T10:30:00Z"
},
"meta": {...}
}
Important: The
tokenfield is only returned at creation time. Store it securely — it cannot be retrieved later. Note that service key tokens have nohot_prefix, making them suitable for white-label use.
Errors:
| Code | Status | Cause |
|---|---|---|
forbidden | 403 | Non-API-key credential attempted to create a service key |
permission_escalation | 403 | Requested permissions exceed parent API key permissions |
List Service Keys
GET /v1/service-keys
Lists service keys for the authenticated API key.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
limit | int | Max results (default: 20) |
offset | int | Pagination offset |
Get Service Key
GET /v1/service-keys/{service_key_id}
Returns details for a specific service key. The key must belong to the authenticated API key.
Revoke Service Key
DELETE /v1/service-keys/{service_key_id}
Revokes a specific service key. The key must belong to the authenticated API key.
Response: 204 No Content
Revoke All Service Keys
DELETE /v1/service-keys
Revokes all active service keys for the authenticated API key.
Response:
{
"data": {
"revoked_count": 3
},
"meta": {...}
}
Custom Domains
Custom domains map your own domain names (e.g., mcp.example.com) to your Hot Dev environment. This feature requires a Pro or Scale subscription plan.
Register Domain
POST /v1/domains
Request Body:
{
"domain": "mcp.example.com"
}
Response: 201 Created
{
"data": {
"domain_id": "550e8400-e29b-41d4-a716-446655440000",
"env_id": "660e8400-e29b-41d4-a716-446655440000",
"domain": "mcp.example.com",
"status": "pending_validation",
"acm_validation_cname_name": "_abc123.mcp.example.com",
"acm_validation_cname_value": "_xyz789.acm-validations.aws",
"cf_distribution_domain": null,
"created_at": "2026-01-15T10:30:00Z"
},
"meta": {...}
}
After creating a domain, add the ACM validation CNAME record (using the acm_validation_cname_name and acm_validation_cname_value fields) to prove domain ownership. Once validated, the cf_distribution_domain field will be populated — add a domain CNAME pointing to the CloudFront distribution to start routing traffic.
Domain statuses: pending_validation, validated, provisioning, active, deleting.
Errors:
| Code | Status | Cause |
|---|---|---|
plan_required | 403 | Custom domains require Pro or Scale plan |
domain_limit_reached | 403 | Domain count limit reached for current plan |
domain_exists | 409 | Domain is already registered |
List Domains
GET /v1/domains
Lists all custom domains for the environment.
Get Domain
GET /v1/domains/{domain_id}
Verify Domain
POST /v1/domains/{domain_id}/verify
Checks the current provisioning status of the domain. If the ACM validation CNAME has propagated and the certificate is issued, the domain moves to validated status and CloudFront provisioning begins. If not yet validated, returns the required DNS records.
Pending domains are also checked automatically in the background, so you don't need to call this endpoint repeatedly.
Response (validated):
{
"data": {
"domain_id": "550e8400-e29b-41d4-a716-446655440000",
"domain": "mcp.example.com",
"status": "validated",
"message": "Domain validated successfully — CloudFront provisioning in progress"
},
"meta": {...}
}
Response (pending):
{
"data": {
"domain_id": "550e8400-e29b-41d4-a716-446655440000",
"domain": "mcp.example.com",
"status": "pending_validation",
"message": "Add a CNAME record: _abc123.mcp.example.com → _xyz789.acm-validations.aws"
},
"meta": {...}
}
Delete Domain
DELETE /v1/domains/{domain_id}
Removes a custom domain. The domain must belong to the authenticated environment. Deletion is asynchronous — the domain enters a deleting state while its CloudFront distribution and TLS certificate are cleaned up, then the record is removed.
Response: 204 No Content
Error Codes
| Code | HTTP Status | Description |
|---|---|---|
unauthorized | 401 | Invalid, missing, expired, or revoked credential |
forbidden | 403 | Credential lacks required permissions |
permission_escalation | 403 | Requested permissions exceed parent credential permissions |
plan_required | 403 | Feature requires a higher subscription plan |
not_found | 404 | Resource not found |
bad_request | 400 | Invalid request body or parameters |
domain_exists | 409 | Custom domain is already registered |
domain_limit_reached | 403 | Domain count limit reached for current plan |
session_limit_exceeded | 429 | Maximum active sessions reached for this API key |
rate_limit_exceeded | 429 | Too many requests (see Retry-After header) |
internal_server_error | 500 | Server error |
Code Examples
curl
# List projects
curl https://api.hot.dev/v1/projects \
-H "Authorization: Bearer $HOT_API_KEY"
# Publish an event
curl -X POST https://api.hot.dev/v1/events \
-H "Authorization: Bearer $HOT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"event_type": "user.signup", "event_data": {"user_id": "123"}}'
JavaScript
const HOT_API_KEY = process.env.HOT_API_KEY;
const BASE_URL = 'https://api.hot.dev/v1';
// List projects
const response = await fetch(`${BASE_URL}/projects`, {
headers: { 'Authorization': `Bearer ${HOT_API_KEY}` }
});
const { data: projects } = await response.json();
// Publish an event
await fetch(`${BASE_URL}/events`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${HOT_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
event_type: 'user.signup',
event_data: { user_id: '123', email: 'alice@example.com' }
})
});
Python
import os
import requests
HOT_API_KEY = os.environ['HOT_API_KEY']
BASE_URL = 'https://api.hot.dev/v1'
headers = {
'Authorization': f'Bearer {HOT_API_KEY}',
'Content-Type': 'application/json'
}
# List projects
response = requests.get(f'{BASE_URL}/projects', headers=headers)
projects = response.json()['data']
# Publish an event
requests.post(
f'{BASE_URL}/events',
headers=headers,
json={
'event_type': 'user.signup',
'event_data': {'user_id': '123', 'email': 'alice@example.com'}
}
)