Developer Docs

Build agents on the CoModel AI API

Authenticate, discover published models, run deterministic queries, and invoke agent action templates — every answer read from a frozen snapshot, with no language model in the loop.

1Overview & how it works

The CoModel AI API is a secure, versioned, machine-to-machine interface for building agents on top of a published decision model. An agent authenticates, discovers the models it is allowed to use, runs deterministic queries against a frozen snapshot, invokes allowlisted action templates, and prepares approved write-backs that it executes in its own system.

Determinism guarantee. Every analytical answer is read from a model's frozen published snapshot by the unchanged CoModel engine. No language model ever produces a number, ranks an option, or invents analysis — repeating a query returns an identical result, and the snapshot's run_id never changes.

Base URL & versioning

Every endpoint lives under https://comodel.ai/v1. The current API version is v1; each response echoes it in the api_version body field and the X-API-Version header.

What you need before you start

2Authentication & tokens

The API uses an OAuth-style client-credentials exchange. You trade your client_id and client_secret for a short-lived Bearer token, then send that token on every subsequent call.

POST/v1/auth/token

Request fields: client_id and client_secret are required. scopes (optional) is a list that must be a subset of the scopes your client is permitted; omit it to receive all of them. expires_in (optional, seconds) may request a shorter token lifetime — tokens default to 1 hour and are capped at 1 hour.

curl -s -X POST https://comodel.ai/v1/auth/token \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "cli_9f3a...",
    "client_secret": "cms_xxxxxxxxxxxxxxxx",
    "scopes": ["models:read", "runs:read", "queries:write", "agents:invoke"]
  }'

Response — the standard success envelope plus the token fields:

{
  "api_version": "v1",
  "request_id": "req_5c1d8e2f4a9b46d7a0c3e1f2",
  "access_token": "eyJ...signed-token...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "models:read runs:read queries:write agents:invoke",
  "token_id": "a1b2c3d4e5f6"
}
The token endpoint is rate limited per client (default 10 requests per minute). Cache and reuse a token until it is close to expiry rather than minting one per call.

Sending the token

Put the token in the Authorization header on every other endpoint:

Authorization: Bearer eyJ...signed-token...
GET/v1/me

Inspect the calling token — useful for confirming which models and scopes it can reach:

{
  "api_version": "v1",
  "request_id": "req_...",
  "client_id": "cli_9f3a...",
  "account_id": "acct_7b21...",
  "scopes": ["models:read", "runs:read", "queries:write", "agents:invoke"],
  "allowed_model_ids": ["mdl_42af..."],
  "token_id": "a1b2c3d4e5f6"
}

3API conventions

Success envelope

Every /v1 success response carries api_version and a unique request_id, plus model_id and run_id where they apply. The endpoint's own data is merged at the top level alongside these fields:

{
  "api_version": "v1",
  "request_id": "req_...",
  "model_id": "mdl_42af...",
  "run_id": "run_88c1...",
  "result": { "...": "endpoint-specific payload" }
}

Response headers

Every /v1 response (success or error) includes X-Request-Id and X-API-Version. Log the request id — it matches the request_id in the body and identifies the call in the audit trail.

Error contract

There is a single error shape across the whole API. Errors never use the success envelope:

{
  "error": {
    "code": "INSUFFICIENT_SCOPE",
    "message": "This token lacks the required scope 'queries:write'.",
    "request_id": "req_...",
    "details": { "required_scope": "queries:write" }
  }
}

Scopes

A token only carries the scopes you were granted. The scopes relevant to building agents on a published model are:

Three further scopes — sources:read, sources:write and sync:run — govern the separate data-ingestion (connector) surface and are not part of building an agent on an already-published model, so they are out of scope for this guide.

Rate limits & payload size

4Discovering models & runs

GET/v1/modelsscope: models:read

List the published models this client is allowed to use.

{
  "api_version": "v1",
  "request_id": "req_...",
  "models": [
    { "model_id": "mdl_42af...", "name": "Retail Margin Model", "status": "published" }
  ],
  "count": 1
}
GET/v1/models/{model_id}scope: models:read

Inspect a single model. run_id is the model's frozen published snapshot; vocabulary_size is the number of API-visible metrics.

{
  "api_version": "v1",
  "request_id": "req_...",
  "model_id": "mdl_42af...",
  "run_id": "run_88c1...",
  "name": "Retail Margin Model",
  "status": "published",
  "vocabulary_size": 12
}
GET/v1/models/{model_id}/runs/{run_id}scope: runs:read

A published model exposes exactly one run — its frozen snapshot. Any other run id returns 404 NOT_FOUND.

{
  "api_version": "v1",
  "request_id": "req_...",
  "model_id": "mdl_42af...",
  "run_id": "run_88c1...",
  "status": "published"
}

5Deterministic queries

POST/v1/models/{model_id}/queryscope: queries:write

Run a deterministic query against the frozen published snapshot. The call is side-effect-free: it only reads the snapshot. The body is { "query_type": ..., "input": { ... } }.

Every query returns a uniform structured result under result:

facts_used is always empty: business facts are inert at publish. Pass the admin:manage scope to additionally receive a raw diagnostics block and advanced_diagnostics: true at the top level.

metric_drivers

What most influences a target metric. Input: target_metric (required); top_k and rank_by (direct | propagated | direct_plus_propagated) are optional.

curl -s -X POST https://comodel.ai/v1/models/mdl_42af.../query \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "query_type": "metric_drivers", "input": { "target_metric": "gross_margin" } }'
{
  "api_version": "v1", "request_id": "req_...",
  "model_id": "mdl_42af...", "run_id": "run_88c1...",
  "advanced_diagnostics": false,
  "result": {
    "query_type": "metric_drivers",
    "summary": "Top 2 driver(s) of gross_margin.",
    "answer": {
      "summary": "Top 2 driver(s) of gross_margin.",
      "drivers": [
        { "metric": "discount_rate", "direction": "negative",
          "direct_effect": -0.41, "propagated_effect": -0.06,
          "total_effect": -0.47, "confidence": 0.82 }
      ]
    },
    "expected_effects": [
      { "metric": "discount_rate", "direction": "negative", "effect": -0.47, "confidence": 0.82 }
    ],
    "direction": null, "confidence": 0.82,
    "traceability": { "metrics_used": ["gross_margin", "discount_rate"],
      "relationships_used": [{ "source": "discount_rate", "target": "gross_margin" }],
      "levers_used": ["discount_rate"], "facts_used": [] },
    "limits": []
  }
}

downstream_impact

What a source metric flows into. Input: source_metric (required).

{
  "result": {
    "query_type": "downstream_impact",
    "summary": "Top 3 downstream impact(s) of discount_rate.",
    "answer": { "summary": "...", "impacts": [
      { "metric": "gross_margin", "direction": "negative", "delta": -0.47 }
    ] },
    "expected_effects": [ { "metric": "gross_margin", "direction": "negative", "effect": -0.47 } ],
    "direction": null, "confidence": null,
    "traceability": { "metrics_used": ["discount_rate", "gross_margin"],
      "relationships_used": [{ "source": "discount_rate", "target": "gross_margin" }],
      "levers_used": ["discount_rate"], "facts_used": [] },
    "limits": []
  }
}

simulate_scenario

Apply a percentage change to a tunable lever and read the propagated effects. Input: lever_metric (required, must be a tunable lever — otherwise 422 LEVER_NOT_TUNABLE); target_metric and delta_pct are optional.

{
  "result": {
    "query_type": "simulate_scenario",
    "summary": "Simulated -5.0% change at discount_rate -> gross_margin delta 0.024.",
    "answer": {
      "summary": "...", "lever_metric": "discount_rate", "target_metric": "gross_margin",
      "delta_pct": -5.0, "target_delta": 0.024, "point_estimate": 0.024,
      "impacts": [ { "metric": "gross_margin", "delta": 0.024 } ],
      "uncertainty_source": "posterior_samples", "uncertainty_band": [0.018, 0.031]
    },
    "expected_effects": [ { "metric": "gross_margin", "effect": 0.024 } ],
    "direction": "positive", "confidence": null,
    "traceability": { "metrics_used": ["discount_rate", "gross_margin"],
      "relationships_used": [], "levers_used": ["discount_rate"], "facts_used": [] },
    "limits": []
  }
}

relationship_explanation

The fused evidence binding one metric to another. Input: source_metric and target_metric (both required).

{
  "result": {
    "query_type": "relationship_explanation",
    "summary": "Relationship evidence for discount_rate -> gross_margin.",
    "answer": { "summary": "...", "relationship": {
      "direction": "negative", "confidence": 0.82, "strength": 0.47 } },
    "expected_effects": [], "direction": "negative", "confidence": 0.82,
    "traceability": { "metrics_used": ["discount_rate", "gross_margin"],
      "relationships_used": [{ "source": "discount_rate", "target": "gross_margin" }],
      "levers_used": ["discount_rate"], "facts_used": [] },
    "limits": []
  }
}

model_health

Model-level diagnostics. No input required.

{
  "result": {
    "query_type": "model_health",
    "summary": "Model diagnostics.",
    "answer": { "summary": "Model diagnostics.", "diagnostics": { "...": "..." } },
    "expected_effects": [], "direction": null, "confidence": null,
    "traceability": { "metrics_used": [], "relationships_used": [],
      "levers_used": [], "facts_used": [] },
    "limits": []
  }
}

decision_binding_check

Whether a source→target edge is decision-grade (safe to plan on). Input: source_metric and target_metric (both required).

{
  "result": {
    "query_type": "decision_binding_check",
    "summary": "discount_rate -> gross_margin: decision_edge_status=promoted, ...",
    "answer": {
      "binding_found": true, "relationship_status": "established",
      "direction_status": "negative", "decision_edge_status": "promoted",
      "w_orientation_safe": true, "planning_decision_safe": true, "reasons": [] },
    "expected_effects": [], "direction": "negative", "confidence": 0.71,
    "traceability": { "metrics_used": ["discount_rate", "gross_margin"],
      "relationships_used": [{ "source": "discount_rate", "target": "gross_margin" }],
      "levers_used": ["discount_rate"], "facts_used": [] },
    "limits": []
  }
}

prepare_agent_context

A one-call bootstrap for an agent: the model's vocabulary, its tunable levers, the query types it supports, and the agent actions available. No input required — start here.

{
  "result": {
    "query_type": "prepare_agent_context",
    "summary": "Agent context: 12 metric(s), 3 tunable lever(s), 7 query type(s), 6 agent action(s).",
    "answer": {
      "run_id": "run_88c1...",
      "vocabulary": [ { "metric_id": "gross_margin", "metric_name": "Gross Margin" } ],
      "tunable_levers": ["discount_rate"],
      "supported_query_types": ["metric_drivers", "downstream_impact",
        "simulate_scenario", "relationship_explanation", "model_health",
        "decision_binding_check", "prepare_agent_context"],
      "agent_actions": [ { "action": "create_recommendation",
        "description": "Recommend the strongest actionable driver for a target metric.",
        "supported_modes": ["analysis_only", "prepare_payload"],
        "requires_approval": false } ]
    },
    "expected_effects": [], "direction": null, "confidence": null,
    "traceability": { "metrics_used": ["discount_rate"], "relationships_used": [],
      "levers_used": ["discount_rate"], "facts_used": [] },
    "limits": []
  }
}

6The agent runtime

This is the core of building an agent. Instead of executing arbitrary code, an agent invokes one of a fixed set of allowlisted action templates. Each template consults the deterministic queries above, reasons over the results without any language model, and returns a structured, auditable result.

POST/v1/models/{model_id}/agents/invokescope: agents:invoke

Body: { "action": ..., "mode": ..., "input": { ... }, "approved": false }. mode defaults to analysis_only.

Action templates

The three safety modes

Automatic downgrade behavior

The runtime fails safe. If a template's required facts are missing (for example a lever that is not tunable in this model), the effective mode is forced to analysis_only and a missing_facts:... entry is added to limits. If approved_writeback is requested without approved: true, it is downgraded and a writeback_not_approved limit is added — it is never silently executed.

The write-back contract

CoModel never performs the external write. The strongest possible outcome is a structured payload returned with executed: false and delivery: "client_must_execute". Your system performs the actual write against your own tools.

An analysis_only invocation (note llm_used: false):

curl -s -X POST https://comodel.ai/v1/models/mdl_42af.../agents/invoke \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "action": "explain_decision",
        "input": { "source_metric": "discount_rate", "target_metric": "gross_margin" } }'
{
  "api_version": "v1", "request_id": "req_...",
  "model_id": "mdl_42af...", "run_id": "run_88c1...",
  "advanced_diagnostics": false,
  "result": {
    "action": "explain_decision",
    "requested_mode": "analysis_only", "effective_mode": "analysis_only",
    "analysis": [ { "query_type": "relationship_explanation", "...": "..." } ],
    "reasoning": ["Relationship evidence for discount_rate -> gross_margin."],
    "traceability": { "model_id": "mdl_42af...", "run_id": "run_88c1...",
      "metrics_used": ["discount_rate", "gross_margin"],
      "relationships_used": [{ "source": "discount_rate", "target": "gross_margin" }],
      "levers_used": ["discount_rate"], "facts_used": [] },
    "limits": [], "llm_used": false
  }
}

An approved write-back — note the payload is returned but executed: false:

curl -s -X POST https://comodel.ai/v1/models/mdl_42af.../agents/invoke \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "action": "prepare_external_writeback", "mode": "approved_writeback",
        "approved": true, "input": { "lever_metric": "discount_rate" } }'
{
  "result": {
    "action": "prepare_external_writeback",
    "requested_mode": "approved_writeback", "effective_mode": "approved_writeback",
    "prepared_payload": { "action": "prepare_external_writeback",
      "input": { "lever_metric": "discount_rate" },
      "model_id": "mdl_42af...", "run_id": "run_88c1...",
      "executed": false, "delivery": "client_must_execute" },
    "writeback": { "approved": true, "executed": false,
      "delivery": "client_must_execute",
      "payload": { "...": "same prepared_payload" } },
    "limits": [], "llm_used": false
  }
}

Decision simulations (supporting endpoint)

Where a published model exposes decision simulation, you can also run a full scenario directly against its frozen snapshot. This is the same deterministic runner the agent flow consults.

POST/v1/models/{model_id}/simulations/run

Body: { "scenario_id": ..., "selected_choice_id": ..., "state_overrides": { ... } }. A model that does not expose simulation returns 422 SIMULATION_NOT_AVAILABLE.

7Build an agent: end to end

Tying it together — from credentials to a write-back your own system executes.

1. Get a token

TOKEN=$(curl -s -X POST https://comodel.ai/v1/auth/token \
  -H "Content-Type: application/json" \
  -d '{ "client_id": "cli_9f3a...", "client_secret": "cms_xxxx",
        "scopes": ["models:read", "queries:write", "agents:invoke"] }' \
  | python3 -c 'import sys,json; print(json.load(sys.stdin)["access_token"])')

2. List the models you can use

curl -s https://comodel.ai/v1/models -H "Authorization: Bearer $TOKEN"

3. Bootstrap the model context

Discover the vocabulary, tunable levers and available actions in one call:

curl -s -X POST https://comodel.ai/v1/models/mdl_42af.../query \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "query_type": "prepare_agent_context" }'

4. Query for drivers

curl -s -X POST https://comodel.ai/v1/models/mdl_42af.../query \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "query_type": "metric_drivers", "input": { "target_metric": "gross_margin" } }'

5. Invoke an agent template

curl -s -X POST https://comodel.ai/v1/models/mdl_42af.../agents/invoke \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "action": "create_recommendation", "mode": "prepare_payload",
        "input": { "target_metric": "gross_margin" } }'

6. Prepare and approve a write-back

curl -s -X POST https://comodel.ai/v1/models/mdl_42af.../agents/invoke \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "action": "prepare_external_writeback", "mode": "approved_writeback",
        "approved": true, "input": { "lever_metric": "discount_rate" } }'

7. Execute it in your own system

CoModel returns the write-back payload with executed: false. Take result.writeback.payload and perform the actual change in your own tool (CRM, planning system, ticketing, etc.). CoModel deliberately never reaches into your systems — you stay in control of every external action.

Because every answer is deterministic and traceable, you can replay any decision: the same token, model and inputs always return the same result, and each call is identified by its request_id in the audit trail.

8Error reference

All errors use the single error contract shown in Conventions. The most common codes: