Admin API Reference
The Admin API provides CRUD operations for trigger rules, tenant overrides, event-source defaults, event schemas, and flows. It runs as a separate process (make run-api) from the Kafka consumer.
Common base URLs:
- direct local binary / YAML config:
http://localhost:8787 - Docker host mapping in this repo:
http://localhost:8090
The process port itself is controlled by api.port / API_PORT. In Docker, the process often listens on 8080 inside the container while the host calls 8090.
Endpoints
Health
| Method | Path | Description |
|---|---|---|
| GET | /health | Health check |
Response: 200 OK
{ "status": "ok" }
Triggers
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/triggers | List all rules |
| POST | /api/v1/triggers | Create a new rule |
| GET | /api/v1/triggers/{id} | Get rule by ID |
| PUT | /api/v1/triggers/{id} | Update rule |
| DELETE | /api/v1/triggers/{id} | Delete rule |
| PUT | /api/v1/triggers/{id}/enabled | Toggle enabled state |
POST /api/v1/triggers
Request body:
{
"name": "New Rule",
"source": { "type": "kafka", "topic": "events-topic" },
"conditions": {
"event_type": "player_first_casino_state",
"expression": "payload.rounds_count > 10"
},
"output_topic": "triggers--my-output",
"output_mapping": {
"mode": "select",
"fields": [{ "source": "player_id", "target": "pid" }]
}
}
idis auto-generated (UUID) if not providedoutput_mappingis optional- CEL
expressionis validated against the schema for theevent_typeif a schema exists
Response: 201 Created — Rule object with generated ID
Errors:
400— Invalid body, invalid CEL expression, or invalid output mapping409— Rule with this ID already exists
PUT /api/v1/triggers/{id}
Request body: Full rule object (ID in path is used, body id is ignored)
Response: 200 OK — Updated rule
Errors:
400— Invalid body, invalid CEL expression, or invalid output mapping404— Rule not found409— Rule is managed by a flow (edit the flow instead)
DELETE /api/v1/triggers/{id}
Response: 204 No Content
Errors:
404— Rule not found409— Rule is managed by a flow (delete the flow instead)
PUT /api/v1/triggers/{id}/enabled
Request body:
{ "enabled": false }
Response: 200 OK
{ "status": "ok" }
Errors: 404 — Rule not found
Tenant Overrides
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/triggers/{id}/tenants | List tenant overrides |
| PUT | /api/v1/triggers/{id}/tenants/{tenant_id} | Upsert tenant override |
| POST | /api/v1/triggers/{id}/tenants/{tenant_id} | Upsert tenant override (alias) |
PUT /api/v1/triggers/{id}/tenants/{tenant_id}
Request body:
{
"tenant_id": "2",
"enabled": true
}
tenant_idin body is ignored; path parameter is usedenabledis optional (omit to keep prior behavior for that tenant)
Response: 200 OK
{ "status": "ok" }
Errors: 404 — Rule not found
Event Sources (default output topic per Kafka source topic)
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/event-sources | List all event source defaults |
| GET | /api/v1/event-sources/{source_topic} | Get by source topic |
| PUT | /api/v1/event-sources/{source_topic} | Upsert default output topic |
The engine uses default_output_topic when a rule's output_topic is empty. After changing data via the API, the consumer reloads caches on its periodic tick.
PUT /api/v1/event-sources/{source_topic}
Request body:
{
"default_output_topic": "triggers--loyalty-rank-up",
"description": "optional"
}
default_output_topicis required (non-empty after trim)
Response: 200 OK
Errors: 400 — Missing/empty default_output_topic
Event Schemas (CEL field definitions per event type)
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/schemas | List all schemas |
| GET | /api/v1/schemas/{event_type} | Get schema by event type |
| PUT | /api/v1/schemas/{event_type} | Upsert schema |
| DELETE | /api/v1/schemas/{event_type} | Delete schema |
Schemas define the fields and types available in CEL expressions for a given event type.
PUT /api/v1/schemas/{event_type}
Request body:
{
"fields": [
{ "name": "player_id", "type": "int" },
{ "name": "rounds_count", "type": "int" },
{ "name": "is_active", "type": "bool" }
]
}
Supported field types: int, double, string, bool.
Response: 200 OK
Errors:
400— Empty fields array or invalid field type404— Schema not found (for GET/DELETE)
CEL Expression Validation
| Method | Path | Description |
|---|---|---|
| POST | /api/v1/validate/expression | Validate a CEL expression |
Request body:
{
"event_type": "player_first_casino_state",
"expression": "payload.rounds_count > 10"
}
Response: 200 OK
{ "valid": true, "errors": [] }
or
{ "valid": false, "errors": ["expression must return bool, got int"] }
Errors:
400— Missing event_type or unknown event type (no schema)
Flows (Studio graph model)
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/flows | List all flows |
| POST | /api/v1/flows | Create a new flow |
| GET | /api/v1/flows/{id} | Get flow by ID |
| PUT | /api/v1/flows/{id} | Update flow |
| DELETE | /api/v1/flows/{id} | Delete flow and related flow-owned data |
| PUT | /api/v1/flows/{id}/enabled | Toggle flow enabled state (propagates to compiled rules) |
| POST | /api/v1/flows/{id}/validate | Validate flow graph structure |
| POST | /api/v1/flows/{id}/compile | Compile flow into engine-executable rules |
POST /api/v1/flows
Request body:
{
"name": "Bad Start Flow",
"source": {
"node_id": "src",
"event_type": "player_first_casino_state",
"source_topic": "player-first-casino-state.v1"
},
"conditions": [
{
"node_id": "c1",
"label": "High Loss",
"expression": "payload.dead_rounds_count > 50"
},
{ "node_id": "c2", "label": "Else", "is_else": true }
],
"outputs": [
{
"node_id": "o1",
"label": "Notify",
"output_topic": "triggers--bad-start-notify"
},
{
"node_id": "o2",
"label": "Log Only",
"output_topic": "triggers--bad-start-log"
}
],
"edges": [
{ "id": "e1", "from": "src", "to": "c1" },
{ "id": "e2", "from": "src", "to": "c2" },
{ "id": "e3", "from": "c1", "to": "o1" },
{ "id": "e4", "from": "c2", "to": "o2" }
]
}
idis auto-generated (UUID) if not providedstatusdefaults to"draft"
Response: 201 Created
POST /api/v1/flows/{id}/validate
Validates the flow graph structure without compiling.
Response: 200 OK
{ "valid": true, "errors": [] }
or
{
"valid": false,
"errors": [
"source: missing event_type",
"output \"o1\": missing output_topic"
]
}
POST /api/v1/flows/{id}/compile
Validates and compiles the flow into flat rules. Deletes any previously compiled rules, creates new ones, and updates the flow status to "active".
Response: 200 OK
{
"status": "compiled",
"rule_ids": ["flow_abc_c1_o1", "flow_abc_c2_o2"],
"rule_count": 2
}
Errors:
400— Validation failed (returns validation errors)404— Flow not found
DELETE /api/v1/flows/{id}
Deletes the flow and all related compiled rules, versions, release attempts, and edit lock.
Response: 204 No Content
Error Format
{ "error": "human-readable message" }
Operational Notes
- At-least-once: Kafka redelivery can cause duplicate outbound publishes; downstream consumers should be idempotent.
- Skip when no topic: If neither rule
output_topicnor a default for the source topic exists, the engine skips dispatch (warn log). - Flow-managed rules: Rules compiled from flows cannot be directly updated or deleted via the triggers API. Edit the flow and recompile instead.
- Compilation is idempotent: Recompiling a flow produces the same deterministic rule IDs (
flow_{flowID}_{condNodeID}_{outNodeID}).
Components catalog
POST /api/v1/components/register
Service-side sdk.RegisterOnBoot POSTs a ServiceManifest here on boot and every 5 minutes after. Payload cap is 1 MiB.
Responses:
200 {"status":"ok"}— manifest accepted (new registration or heartbeat).400— malformed manifest (validation, invalid actionconfig_schema, etc.).409— immutability violation: a component with the same(service, id, version)already exists with different content.413— payload exceeds 1 MiB cap.
GET /api/v1/components
Returns every row of triggers_component_registry sorted by (service, component_id, version). Each row is a ComponentManifestRow wrapping a ComponentEntry plus first_registered_at, last_seen_at, and (when a previous version existed) a triggers-computed server_compatibility report.
GET /api/v1/components/:id/data/:source
Proxies a Studio form query through to the owning service's HTTP endpoint. The triggers server:
- Looks up the latest manifest row for component
:id. - Resolves
data_sources[name=:source]on that entry. - Joins the service's
transports.http.endpointwith the data-source'spath. - Forwards only the query params listed in
data_sources[*].params(anything else is dropped). - Returns the upstream body plus the upstream
Content-Type.
Upstream non-2xx yields 502 Bad Gateway. Unknown :id or :source also yields 502 with a descriptive error.