Cloud API
The Restormel Keys Cloud API is split across two surfaces: a Zuplo Gateway API for control-plane CRUD (projects, keys), and a Dashboard API for runtime operations (Resolve, policy evaluate, routes/steps). Restormel Keys is library-first — you keep your existing infrastructure for execution (OpenRouter/Portkey/Vercel AI/direct providers), and Restormel adds the application layer (BYOK, routing, policies).
Two API surfaces (and two key types)
Restormel Keys has two distinct HTTP surfaces:
- Dashboard API (runtime operations) — resolve, policies/evaluate, routes/steps, catalog, project model index (read + write).
Call
https://restormel.dev/keys/dashboard/api/...with your Gateway Key (rk_...) from your backend. - Zuplo Gateway API (control-plane CRUD) — health, projects, project keys.
Call
https://restormel-keys-gateway-main-bc13eba.zuplo.app/api/...with a consumer key (zpka_...).
Matrix: project model index (picker / merge source)
Hosts (e.g. Sophia) call this list to drive model pickers and to merge with a static catalog. Use the same canonical providerType values as resolve and route steps (google vs vertex is spelled out in Resolve → execution contract).
Integrator reference (stable GET data shape, project_models_validation_failed, global catalog vs index): keys-catalog-sync.md.
| Operation | Surface | Credential | Status |
|---|---|---|---|
GET .../projects/{projectId}/models | Dashboard API | Gateway Key (rk_...) | Bindings in JSON data (array; not data.bindings) including bindingKind (execution vs registry) + nested catalog rows when known. Legacy ?source=catalog deprecated — prefer GET /api/models. Batch writes: on failure, project_models_validation_failed + errors[]. |
POST .../models (batch add), PUT .../models (replace allowlist) | Dashboard API | Gateway Key (rk_...) | Idempotent add per providerType + modelId. Optional bindingKind: execution (default) = catalog + canonical provider + variants; registry = opaque strings
for host merge when off-catalog. |
PATCH .../models/{bindingId} (enabled), DELETE .../models/{bindingId} | Dashboard API | Gateway Key (rk_...) | Soft-disable vs hard-remove. Binding id from GET. |
| Same operations via Zuplo | Zuplo Gateway | zpka_... | Not used for this index — do not assume consumer-key paths for project model bindings. |
Project model index (curl)
List bindings (default):
curl -sS \
"https://restormel.dev/keys/dashboard/api/projects/${RESTORMEL_PROJECT_ID}/models" \
-H "Authorization: Bearer ${RESTORMEL_GATEWAY_KEY}" | jq '.meta.source, (.data | length)'Add (batch):
curl -sS -X POST \
"https://restormel.dev/keys/dashboard/api/projects/${RESTORMEL_PROJECT_ID}/models" \
-H "Authorization: Bearer ${RESTORMEL_GATEWAY_KEY}" \
-H "Content-Type: application/json" \
-d '{"models":[{"providerType":"vertex","modelId":"text-embedding-005"}]}' | jq '.data'Replace full allowlist (declarative sync):
curl -sS -X PUT \
"https://restormel.dev/keys/dashboard/api/projects/${RESTORMEL_PROJECT_ID}/models" \
-H "Authorization: Bearer ${RESTORMEL_GATEWAY_KEY}" \
-H "Content-Type: application/json" \
-d '{"models":[{"providerType":"vertex","modelId":"text-embedding-005","enabled":true}]}' | jq '.data'Disable one binding / delete one binding (set RESTORMEL_BINDING_ID from GET):
curl -sS -X PATCH \
"https://restormel.dev/keys/dashboard/api/projects/${RESTORMEL_PROJECT_ID}/models/${RESTORMEL_BINDING_ID}" \
-H "Authorization: Bearer ${RESTORMEL_GATEWAY_KEY}" \
-H "Content-Type: application/json" \
-d '{"enabled":false}' | jq '.data'curl -sS -X DELETE \
"https://restormel.dev/keys/dashboard/api/projects/${RESTORMEL_PROJECT_ID}/models/${RESTORMEL_BINDING_ID}" \
-H "Authorization: Bearer ${RESTORMEL_GATEWAY_KEY}" | jq '.'Canonical catalog (Dashboard API, public)
No Gateway Key required. Returns versioned provider and model metadata for BYOK UIs and integrations.
Use GET /keys/dashboard/api/catalog with optional limit, offset, and includeUnhealthy=1 (operators).
curl -sS "https://restormel.dev/keys/dashboard/api/catalog?limit=5" | jq '.contractVersion, .generatedAt'Step-by-step for third parties: Canonical model & provider catalog.
Resolve API (Dashboard API)
Restormel Resolve is the core runtime operation: your backend asks Restormel which provider/model to use for a request. Restormel does not proxy the AI request — it returns routing instructions; your backend calls the provider directly.
Endpoint
POST /keys/dashboard/api/projects/{projectId}/resolve
Authorization: Bearer {RESTORMEL_GATEWAY_KEY}
Content-Type: application/jsonRequest body
{ "environmentId": "prod", "routeId": "ingestion-extract" }environmentId(required): environment to resolve against (e.g.dev,prod)routeId(optional): route name to evaluate; if omitted, uses the first active route for that environment
curl example
curl -s -X POST \
"https://restormel.dev/keys/dashboard/api/projects/${RESTORMEL_PROJECT_ID}/resolve" \
-H "Authorization: Bearer ${RESTORMEL_GATEWAY_KEY}" \
-H "Content-Type: application/json" \
-d '{ "environmentId": "prod", "routeId": "ingestion-extract" }' | jq '.data'Response (200)
{
"data": {
"contractVersion": "2026-03-25",
"traceId": "trace_123",
"routeId": "route_abc123",
"routeName": "ingestion-extract",
"providerType": "anthropic",
"modelId": "claude-3-5-sonnet",
"explanation": "route=... step=0 provider=anthropic model=claude-3-5-sonnet",
"decisionMetadata": { "selectedStepId": "step_01", "switchReasonCode": null }
}
}data.routeId: the matched route id (stable identifier)data.routeName: the matched route display namedata.providerType: the provider to call (e.g.openai,anthropic)data.modelId: the model to use (may benull)data.explanation: human-readable trace for debugging
Error responses
| HTTP | Error | Meaning |
|---|---|---|
| 401 | unauthorized | Gateway Key missing or invalid |
| 403 | forbidden | Gateway Key project does not match projectId |
| 404 | no_route | No active route found for this environment / route name |
| 500 | internal_error | Unexpected resolver/runtime failure; inspect logs and route configuration |
Policy evaluate (Dashboard API)
Use policy evaluate to test whether a model/provider combination is allowed by active policies without executing a full resolve.
Endpoint
POST /keys/dashboard/api/policies/evaluate
Authorization: Bearer {RESTORMEL_GATEWAY_KEY}
Content-Type: application/jsonRequest body
{ "projectId": "...", "environmentId": "prod", "modelId": "gpt-4o", "providerType": "openai" }When authenticating with a Gateway Key, the server enforces that projectId matches the key’s bound project.
Never trust client-supplied projectId for cross-project evaluation.
curl example
curl -s -X POST \
"https://restormel.dev/keys/dashboard/api/policies/evaluate" \
-H "Authorization: Bearer ${RESTORMEL_GATEWAY_KEY}" \
-H "Content-Type: application/json" \
-d '{ "projectId": "'${RESTORMEL_PROJECT_ID}'", "environmentId": "prod", "modelId": "gpt-4o", "providerType": "openai" }' \
| jq '.data'Response
{
"data": {
"allowed": true,
"violations": []
}
}Ingestion routing control-plane (Dashboard API)
The Dashboard API now exposes route-level metadata and simulation surfaces used by ingestion routing UIs. These endpoints are project-scoped and require a Gateway Key.
Route simulation
Endpoint
POST /keys/dashboard/api/projects/{projectId}/routes/{routeId}/simulate
Authorization: Bearer {RESTORMEL_GATEWAY_KEY}
Content-Type: application/jsonRequest body (example)
{ "environmentId": "prod", "stage": "ingestion_grouping", "workload": "ingestion", "estimatedInputTokens": 12000 }curl example
curl -s -X POST \
"https://restormel.dev/keys/dashboard/api/projects/${RESTORMEL_PROJECT_ID}/routes/${RESTORMEL_ROUTE_ID}/simulate" \
-H "Authorization: Bearer ${RESTORMEL_GATEWAY_KEY}" \
-H "Content-Type: application/json" \
-d '{ "environmentId": "prod", "stage": "ingestion_grouping", "workload": "ingestion", "estimatedInputTokens": 12000 }' \
| jq '.data'Response (example)
{
"data": {
"contractVersion": "2026-03-25",
"traceId": "trace_123",
"routeId": "route_abc123",
"routeName": "ingestion-extract",
"selectedStepId": "step_01",
"estimatedCostUsd": 0.18,
"perStepEstimates": [
{ "stepId": "step_01", "orderIndex": 0, "providerType": "anthropic", "modelId": "claude-sonnet-4", "estimatedCostUsd": 0.18, "wouldRun": true, "wouldBeSkippedBecause": null },
{ "stepId": "step_02", "orderIndex": 1, "providerType": "openai", "modelId": "gpt-4.1", "estimatedCostUsd": 0.21, "wouldRun": false, "wouldBeSkippedBecause": "not_selected" }
],
"switchOutcomePreview": {
"attemptNumber": 0,
"failureKind": null,
"selectedOrderIndex": 0
},
"decisionMetadata": { "selectedStepId": "step_01" }
}
}Routing metadata endpoints
| Endpoint | Purpose |
|---|---|
GET /keys/dashboard/api/policies | List policies visible in your current auth scope (Gateway Key returns project-scoped policies). |
GET /keys/dashboard/api/policies/{id} | Get one policy in current auth scope (used to validate lifecycle with real policy ids). |
GET /keys/dashboard/api/projects/{projectId}/routing-capabilities | Returns workload + stage enums for route editors. |
GET /keys/dashboard/api/projects/{projectId}/models | Project model index: GET/POST/PUT; per-row PATCH/DELETE under .../models/{bindingId} (Gateway Key). |
GET /keys/dashboard/api/projects/{projectId}/providers/health | Provider integration verification/health summary with operator status/reason and explicit empty-state metadata. |
GET /keys/dashboard/api/projects/{projectId}/switch-criteria-enums | Machine-readable switch-criteria enums for UI constraints. |
Stage-aware resolve request fields
Resolve now accepts optional stage/switch context fields to support ingestion workflows: stage, workload, task, attemptNumber, estimatedInputTokens, estimatedInputChars, complexity, constraints, previousFailure, and failureKind.
Resolve responses may include selectedStepId, selectedOrderIndex, and switchReasonCode for operator/trace UX, plus machine-readable fields estimatedCostUsd, matchedCriteria, and fallbackCandidates.
Route lifecycle endpoints
POST /keys/dashboard/api/projects/{projectId}/routes/{routeId}/publish— publish a new route version snapshot.POST /keys/dashboard/api/projects/{projectId}/routes/{routeId}/rollback— rollback to a previously published snapshot (optionaltoVersion).GET /keys/dashboard/api/projects/{projectId}/routes/{routeId}/history— list publish/rollback history events.
Operator advisory endpoints
GET /keys/dashboard/api/projects/{projectId}/route-coverage— stage/workload coverage matrix and zero-enabled-step routes.GET /keys/dashboard/api/projects/{projectId}/readiness— status, issues, and recommendations before rollout.POST /keys/dashboard/api/projects/{projectId}/routes/{routeId}/recommend— route diagnostics, recommendations, and diff preview.GET /keys/dashboard/api/policies/{id}/history,POST /publish,POST /rollback,POST /diff— policy lifecycle parity endpoints.
Policy lifecycle examples
Discover policy ids (Gateway Key scoped)
curl -s \
"https://restormel.dev/keys/dashboard/api/policies" \
-H "Authorization: Bearer ${RESTORMEL_GATEWAY_KEY}" | jq '.data'Use GET /keys/dashboard/api/policies to discover policy ids in your Gateway Key project scope,
then call lifecycle endpoints with a real id.
History
curl -s "https://restormel.dev/keys/dashboard/api/policies/${RESTORMEL_POLICY_ID}/history?limit=20" -H "Authorization: Bearer ${RESTORMEL_GATEWAY_KEY}" | jq '.data'Publish
curl -s -X POST \
"https://restormel.dev/keys/dashboard/api/policies/${RESTORMEL_POLICY_ID}/publish" \
-H "Authorization: Bearer ${RESTORMEL_GATEWAY_KEY}" \
-H "Content-Type: application/json" | jq '.data'Rollback
curl -s -X POST \
"https://restormel.dev/keys/dashboard/api/policies/${RESTORMEL_POLICY_ID}/rollback" \
-H "Authorization: Bearer ${RESTORMEL_GATEWAY_KEY}" \
-H "Content-Type: application/json" \
-d '{ "toVersion": 1 }' | jq '.data'Diff
curl -s -X POST \
"https://restormel.dev/keys/dashboard/api/policies/${RESTORMEL_POLICY_ID}/diff" \
-H "Authorization: Bearer ${RESTORMEL_GATEWAY_KEY}" \
-H "Content-Type: application/json" \
-d '{ "fromVersion": 1, "toVersion": 2 }' | jq '.data'Where to use it
- API reference and Try it: Restormel Keys Developer Portal — full endpoint list, request/response schemas, and interactive “Try it” with a consumer key.
- Gateway base URL (for API calls):
https://restormel-keys-gateway-main-bc13eba.zuplo.app— call/api/health,/api/projects,/api/projects/{id},/api/projects/{id}/keys. The gateway root (/) is not a page; use these paths. - Dashboard: Dashboard — Sign in, create projects, and create Gateway keys for project-scoped access.
How it fits together
- You use the Dashboard to create projects and Gateway keys.
- You (or your services) call the gateway with a consumer key (
zpka_...) from Zuplo's API Key Service. - The Developer Portal (link above) documents the API and lets you try requests.
See the portal's How it all fits together page for the full picture.
Quick links
- Developer Portal (API reference + Try it)
- Introduction & authentication
- Dashboard — create projects and Gateway keys