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.

OperationSurfaceCredentialStatus
GET .../projects/{projectId}/modelsDashboard APIGateway 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 APIGateway 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 APIGateway Key (rk_...)Soft-disable vs hard-remove. Binding id from GET.
Same operations via ZuploZuplo Gatewayzpka_...Not used for this index — do not assume consumer-key paths for project model bindings.

Project model index (curl)

List bindings (default):

bash
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):

bash
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):

bash
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):

bash
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'
bash
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 '.'
Security — Never send a Gateway Key to the browser. Use it only server-side.

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).

bash
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

text
POST /keys/dashboard/api/projects/{projectId}/resolve
Authorization: Bearer {RESTORMEL_GATEWAY_KEY}
Content-Type: application/json

Request body

json
{ "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

bash
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)

json
{
  "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 name
  • data.providerType: the provider to call (e.g. openai, anthropic)
  • data.modelId: the model to use (may be null)
  • data.explanation: human-readable trace for debugging

Error responses

HTTPErrorMeaning
401unauthorizedGateway Key missing or invalid
403forbiddenGateway Key project does not match projectId
404no_routeNo active route found for this environment / route name
500internal_errorUnexpected 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

text
POST /keys/dashboard/api/policies/evaluate
Authorization: Bearer {RESTORMEL_GATEWAY_KEY}
Content-Type: application/json

Request body

json
{ "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

bash
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

json
{
  "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

text
POST /keys/dashboard/api/projects/{projectId}/routes/{routeId}/simulate
Authorization: Bearer {RESTORMEL_GATEWAY_KEY}
Content-Type: application/json

Request body (example)

json
{ "environmentId": "prod", "stage": "ingestion_grouping", "workload": "ingestion", "estimatedInputTokens": 12000 }

curl example

bash
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)

json
{
  "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

EndpointPurpose
GET /keys/dashboard/api/policiesList 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-capabilitiesReturns workload + stage enums for route editors.
GET /keys/dashboard/api/projects/{projectId}/modelsProject model index: GET/POST/PUT; per-row PATCH/DELETE under .../models/{bindingId} (Gateway Key).
GET /keys/dashboard/api/projects/{projectId}/providers/healthProvider integration verification/health summary with operator status/reason and explicit empty-state metadata.
GET /keys/dashboard/api/projects/{projectId}/switch-criteria-enumsMachine-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 (optional toVersion).
  • 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)

bash
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

bash
curl -s "https://restormel.dev/keys/dashboard/api/policies/${RESTORMEL_POLICY_ID}/history?limit=20" -H "Authorization: Bearer ${RESTORMEL_GATEWAY_KEY}" | jq '.data'

Publish

bash
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

bash
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

bash
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: DashboardSign in, create projects, and create Gateway keys for project-scoped access.

How it fits together

  1. You use the Dashboard to create projects and Gateway keys.
  2. You (or your services) call the gateway with a consumer key (zpka_...) from Zuplo's API Key Service.
  3. 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

See also

  • Dashboard — create projects and Gateway keys
  • Sign in — authenticate with GitHub
  • Docs — framework compatibility and guides
  • Pricing — tiers and plans