MuviaDevelopers
Guides: Agent quickstart

Agent quickstart

This page takes an AI agent from nothing to a first write, using only an mvk_ API key and the domain muviabi.com. Every request below is complete, and so is every response (long lists are shortened to one or two items). Read it top to bottom once; you will not have to guess.

The same text is available as Markdown at https://developers.muviabi.com/guides/agent-quickstart.md, and https://developers.muviabi.com/llms.txt indexes every guide.

Endpoints

URL
REST base URL https://api.muviabi.com/api/public
MCP URL https://api.muviabi.com/mcp/v1

Keep the key in an environment variable and never paste it into code, logs or prompts:

export MUVIA_API_KEY="mvk_<key_id>_<secret>"

Part 1: the REST API

The REST API is read-only and returns a company's projects, datasets and rows.

1. Who am I: GET /v1/whoami

Any valid key answers, whatever its scopes. It counts toward the key's per-minute limit but not toward the company's daily quota, so call it first and whenever you are unsure what the key allows.

curl -s -H "X-Api-Key: $MUVIA_API_KEY" https://api.muviabi.com/api/public/v1/whoami
{
  "key": {
    "id": "k7Qp2xVd",
    "name": "Reporting agent",
    "scopes": ["settings_read", "datasets_read", "nodes_read"],
    "expires_at": "2027-01-15T00:00:00Z"
  },
  "company": { "id": "6aa95e431b5b18b1850a2a21", "name": "ACME srl" },
  "limits": {
    "requests_per_minute_per_key": 120,
    "requests_per_day_per_company": 100000,
    "requests_today_company": 412,
    "rows_page_size_max": 1000,
    "page_size_max": 100
  },
  "mcp": { "url": "https://api.muviabi.com/mcp/v1", "available": true, "writes": false },
  "links": {
    "docs": "https://developers.muviabi.com",
    "llms_txt": "https://developers.muviabi.com/llms.txt",
    "openapi": "https://api.muviabi.com/api/public/openapi.json"
  }
}

2. The company: GET /v1/company

Needs settings_read.

curl -s -H "X-Api-Key: $MUVIA_API_KEY" https://api.muviabi.com/api/public/v1/company
{ "id": "6aa95e431b5b18b1850a2a21", "name": "ACME srl" }

mcp.writes in whoami says whether this key may propose or make changes through MCP: true once an administrator has turned on «Can write through assistants» on the key and it holds a permission a change needs. REST itself only reads. Even a key that writes always proposes some changes, for a person to confirm: analyses, links, annotations, a new project (see Writes by a key).

3. List projects: GET /v1/projects

Needs settings_read.

curl -s -H "X-Api-Key: $MUVIA_API_KEY" "https://api.muviabi.com/api/public/v1/projects?size=2"
{
  "items": [
    {
      "id": "6abaad91a7a10614c38f70d5",
      "name": "Energy Dome Demo",
      "description": "Simulated CO2 battery plant",
      "is_default": false,
      "created_at": "2026-09-28T18:51:14.352000",
      "updated_at": "2026-10-12T12:41:19.085000"
    },
    {
      "id": "6aa95e431b5b18b1850a2a22",
      "name": "Project 1",
      "description": null,
      "is_default": true,
      "created_at": "2026-09-01T09:00:00.000000",
      "updated_at": "2026-09-01T09:00:00.000000"
    }
  ],
  "total": 29,
  "page": 1,
  "size": 2,
  "pages": 15
}

4. List a project's datasets: GET /v1/projects/{project_id}/datasets

Needs datasets_read. Note each dataset's id and its columns.

curl -s -H "X-Api-Key: $MUVIA_API_KEY" \
  "https://api.muviabi.com/api/public/v1/projects/6abaad91a7a10614c38f70d5/datasets?size=1"
{
  "items": [
    {
      "id": "6abab724a7a10614c38f731c",
      "name": "Compressor stops (10 min or more)",
      "description": "Every period in which the compression train is stopped, with start and duration.",
      "project_id": "6abaad91a7a10614c38f70d5",
      "is_physical": false,
      "columns": [
        { "name": "stop_id", "type": "Int64" },
        { "name": "started_at", "type": "DateTime('Europe/Rome')" },
        { "name": "duration_seconds", "type": "Int64" }
      ],
      "created_at": "2026-09-28T18:51:09.626000",
      "updated_at": "2026-10-12T12:41:19.444000"
    }
  ],
  "total": 14,
  "page": 1,
  "size": 1,
  "pages": 14
}

5. Read rows: GET /v1/projects/{projectid}/datasets/{datasetid}/rows

Needs datasets_read. Rows are paged with page and size (up to 1000); keep reading while has_more is true. See Pagination.

curl -s -H "X-Api-Key: $MUVIA_API_KEY" \
  "https://api.muviabi.com/api/public/v1/projects/6abaad91a7a10614c38f70d5/datasets/6abab724a7a10614c38f731c/rows?size=2"
{
  "columns": ["stop_id", "started_at", "duration_seconds"],
  "items": [
    { "stop_id": 2, "started_at": "2026-09-29T01:14:02.164000", "duration_seconds": 21600 },
    { "stop_id": 64, "started_at": "2026-10-14T06:18:50.258000", "duration_seconds": 21600 }
  ],
  "page": 1,
  "size": 2,
  "has_more": true
}

Every REST response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (see Rate limits). If anything fails, the answer is the error envelope, with a hint that says what to do next.

6. What REST cannot do

REST has no query endpoint and no series endpoint. The rows endpoint pages a dataset oldest first, so it is the wrong tool for "the latest values" or "a time range". To read a time range, or to run any query, use MCP: call describe_sql, then query_sql (step 11). For example, the last day of a dataset, newest first:

SELECT started_at, duration_seconds
FROM `Compressor stops (10 min or more)_view`
WHERE started_at >= now() - INTERVAL 1 DAY
ORDER BY started_at DESC
LIMIT 100

Part 2: the MCP server

MCP gives an agent far more than REST: hundreds of tools, a SQL query tool and (with permission) writes. See MCP server for the model; here is the wire exchange. MCP speaks JSON-RPC over HTTP POST. Every request carries these headers:

Authorization: Bearer $MUVIA_API_KEY
Content-Type: application/json
Accept: application/json, text/event-stream

The server answers as a server-sent event: one event: message line, then one data: {...} line holding the JSON-RPC response. The examples show the JSON only.

7. initialize

curl -s https://api.muviabi.com/mcp/v1 \
  -H "Authorization: Bearer $MUVIA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"my-agent","version":"1.0"}}}'
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "logging": {},
      "prompts": { "listChanged": true },
      "resources": { "subscribe": false, "listChanged": true },
      "tools": { "listChanged": true }
    },
    "serverInfo": { "name": "muvia", "version": "3.4.7" },
    "instructions": "Muvia is an industrial data platform. Tools act as the connected person, in the company they connected, within the scopes they granted.\nCall whoami first, then list_projects: every other tool acts inside one project, whose time zone and rules describe_project tells.\nfind_tools names the tools of an area (datasets, signals, nodes, alarms, dashboards, analyses, reports, documents, predictors) when your client cannot search its tool list.\nPrefer the curated reads; use query_sql, after describe_sql, only when they are not enough."
  }
}

Then tell the server you are ready. It answers 202 with no body:

curl -s -o /dev/null -w "%{http_code}\n" https://api.muviabi.com/mcp/v1 \
  -H "Authorization: Bearer $MUVIA_API_KEY" -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","method":"notifications/initialized"}'

8. tools/list

curl -s https://api.muviabi.com/mcp/v1 \
  -H "Authorization: Bearer $MUVIA_API_KEY" -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'

The answer lists every tool with its name, description, inputSchema and annotations. Two entries, shortened:

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "tools": [
      {
        "name": "whoami",
        "title": "Who am I connected as",
        "description": "Who this connection acts as in Muvia: the person or API key, the company, the scopes granted and the client. Call it first to know what the other tools can do.",
        "inputSchema": { "type": "object", "properties": {}, "additionalProperties": false },
        "annotations": { "readOnlyHint": true, "openWorldHint": false }
      },
      {
        "name": "annotate_signal",
        "description": "Propose a note on the signals over a stretch of time, among the project's annotations; a person confirms it through approve_url unless the company applies it.",
        "inputSchema": {
          "type": "object",
          "properties": {
            "project_id": { "type": "string" },
            "signals": { "type": "array", "items": { "type": "string" }, "minItems": 1, "maxItems": 12 },
            "start": { "type": "string", "format": "date-time" },
            "end": { "type": "string", "format": "date-time" },
            "text": { "type": "string", "minLength": 1, "maxLength": 2000 },
            "label": { "type": "string", "enum": ["event", "maintenance", "anomaly", "note"] }
          },
          "required": ["project_id", "signals", "start", "end", "text"],
          "additionalProperties": false
        },
        "annotations": { "destructiveHint": false, "openWorldHint": false }
      }
    ]
  }
}

readOnlyHint: true marks a read. A tool without it is a write (see the next steps). destructiveHint: true marks the writes that delete or cannot be undone. Every tool, with its arguments, is also listed for reading on MCP tools (as Markdown at https://developers.muviabi.com/mcp-tools.md).

9. Call whoami

curl -s https://api.muviabi.com/mcp/v1 \
  -H "Authorization: Bearer $MUVIA_API_KEY" -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"whoami","arguments":{}}}'

A tool answer carries the result twice: as text in content and as an object in structuredContent. Read structuredContent.

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [{ "type": "text", "text": "{\"kind\":\"api_key\", ...}" }],
    "structuredContent": {
      "kind": "api_key",
      "company": { "id": "6aa95e431b5b18b1850a2a21", "name": "ACME srl" },
      "user": null,
      "client": null,
      "scopes": ["datasets_read", "settings_read", "analysis_annotate"],
      "permissions": ["datasets_read", "settings_read", "analysis_annotate"],
      "key": { "name": "Reporting agent", "created_by": "Anna Rossi" },
      "writes": true
    },
    "isError": false
  }
}

writes is a plain boolean: true when an administrator has turned on «Can write through assistants» on this key. For a key, key holds its name and created_by. See MCP server.

10. Find a project: list_projects

curl -s https://api.muviabi.com/mcp/v1 \
  -H "Authorization: Bearer $MUVIA_API_KEY" -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"list_projects","arguments":{}}}'
{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "structuredContent": {
      "columns": ["id", "name", "description"],
      "rows": [
        ["6abaad91a7a10614c38f70d5", "Energy Dome Demo", "Simulated CO2 battery plant"],
        ["6aa95e431b5b18b1850a2a22", "Project 1", null]
      ]
    },
    "isError": false
  }
}

Tables come back as columns plus rows (arrays in column order).

11. Run a query: query_sql

Call describe_sql first to learn which tables the project's SQL can read; it answers {"columns":["name","read_in_sql_as"],"rows":[...]}, and the second column is the name to write in FROM, exactly as given and in backticks: a dataset is usually its name, character for character, plus _view (Compressor stops (10 min or more)_view), a source a name of its own. describe_sql is the only source of table names. Then run one SELECT:

curl -s https://api.muviabi.com/mcp/v1 \
  -H "Authorization: Bearer $MUVIA_API_KEY" -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"query_sql","arguments":{"project_id":"6abaad91a7a10614c38f70d5","sql":"SELECT stop_id, started_at, duration_seconds FROM `Compressor stops (10 min or more)_view` ORDER BY started_at LIMIT 2","limit":2}}}'
{
  "jsonrpc": "2.0",
  "id": 6,
  "result": {
    "structuredContent": {
      "columns": ["stop_id", "started_at", "duration_seconds"],
      "rows": [
        [2, "2026-09-29T03:14:02+02:00", 21600],
        [4, "2026-09-29T15:14:02+02:00", 21600]
      ],
      "truncated": false,
      "zone": "Europe/Rome"
    },
    "isError": false
  }
}

To read a time range, put a WHERE on the time column and ORDER BY it DESC with a LIMIT, as in step 6. truncated: true means more rows matched than limit returned. zone is the project's time zone, in which instants are written. query_sql only ever runs a read-only SELECT.

12. A write: annotate_signal

Writes use the same tools/call. Here the agent asks Muvia to note an event on a signal:

curl -s https://api.muviabi.com/mcp/v1 \
  -H "Authorization: Bearer $MUVIA_API_KEY" -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"annotate_signal","arguments":{"project_id":"6abaad91a7a10614c38f70d5","signals":["compressor_power"],"start":"2026-09-29T01:14:02Z","end":"2026-09-29T07:14:02Z","text":"Planned stop for filter change","label":"maintenance"}}}'

What comes back depends on the company's policy for this kind of change, and on the key (see MCP server).

Awaiting confirmation («Ask for confirmation», the usual policy). Nothing has changed yet; a person approves the proposal in Muvia by opening approve_url:

{
  "jsonrpc": "2.0",
  "id": 7,
  "result": {
    "structuredContent": {
      "status": "pending",
      "action_id": "6ad9f3c24e5b8a1d7c0f2b19",
      "approve_url": "https://app.muviabi.com/approvals/6ad9f3c24e5b8a1d7c0f2b19?project=6abaad91a7a10614c38f70d5",
      "summary": "Add a maintenance note to 1 signal, 29 Sep 2026 03:14 to 09:14",
      "needs_admin": false
    },
    "isError": false
  }
}

Applied at once («Run immediately»):

{
  "jsonrpc": "2.0",
  "id": 7,
  "result": {
    "structuredContent": {
      "status": "applied",
      "action_id": "6ad9f3c24e5b8a1d7c0f2b19",
      "id": "6ad9f3c24e5b8a1d7c0f2b1a",
      "url": "https://app.muviabi.com/analysis?project=6abaad91a7a10614c38f70d5"
    },
    "isError": false
  }
}

Calling the same write again within seconds returns the same answer instead of writing twice.

13. Worked example: create a dashboard and an analysis

Both tools are writes, so both answer with a proposal or an applied change (see below). First find the ids they need.

Signal ids: list_signals returns one row per signal; the id is what create_analysis takes. Signal ids are long and structured (tag:<node>:<source>:<name>); copy them exactly.

curl -s https://api.muviabi.com/mcp/v1 \
  -H "Authorization: Bearer $MUVIA_API_KEY" -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":9,"method":"tools/call","params":{"name":"list_signals","arguments":{"project_id":"6abaad91a7a10614c38f70d5","limit":3}}}'
{
  "columns": ["id", "name", "unit", "last_value", "last_at", "untrackable_reason"],
  "rows": [
    ["tag:16e0bd33-a313-4009-8af7-002a5be938b2:a05b0667-1e81-4745-a25f-3537eb600fad:AMB_T_C", "AMB_T_C", null, 22.67, "2026-10-19T21:05:34+00:00", null],
    ["tag:16e0bd33-a313-4009-8af7-002a5be938b2:a05b0667-1e81-4745-a25f-3537eb600fad:AUX_P_MW", "AUX_P_MW", null, 0.87, "2026-10-19T21:05:34+00:00", null]
  ],
  "truncated": true,
  "next_offset": 3
}

create_analysis. Input schema, from tools/list: project_id and title (1 to 120 characters) are required; signals (1 to 12 ids from list_signals), start and end (ISO 8601) give one trend; or definition gives a full analysis in the shape get_analysis returns; optional description (up to 2000 characters) and labels (up to 10).

{
  "jsonrpc": "2.0",
  "id": 10,
  "method": "tools/call",
  "params": {
    "name": "create_analysis",
    "arguments": {
      "project_id": "6abaad91a7a10614c38f70d5",
      "title": "Ambient temperature and auxiliary power",
      "signals": [
        "tag:16e0bd33-a313-4009-8af7-002a5be938b2:a05b0667-1e81-4745-a25f-3537eb600fad:AMB_T_C",
        "tag:16e0bd33-a313-4009-8af7-002a5be938b2:a05b0667-1e81-4745-a25f-3537eb600fad:AUX_P_MW"
      ],
      "start": "2026-10-12T00:00:00Z",
      "end": "2026-10-19T00:00:00Z",
      "description": "Last week, one trend"
    }
  }
}

create_dashboard. project_id, name and widgets are required. Each widget is bound to a dataset's columns (never to SQL of its own): {title, type, dataset_id, channels, chart_kind?, reduce?}, where type is one of CHART, NUMBER, LABEL, ALARM, ENUM, TABLE, dataset_id comes from list_datasets, and channels maps each channel the type reads (x and y for a line chart) to column names of that dataset.

{
  "jsonrpc": "2.0",
  "id": 11,
  "method": "tools/call",
  "params": {
    "name": "create_dashboard",
    "arguments": {
      "project_id": "6abaad91a7a10614c38f70d5",
      "name": "Daily charge energy",
      "widgets": [
        {
          "title": "Energy absorbed per day",
          "type": "CHART",
          "chart_kind": "line",
          "dataset_id": "6abab71da7a10614c38f72f0",
          "channels": { "x": "bucket", "y": ["consumption"] }
        }
      ]
    }
  }
}

Outcome 1: a proposal awaiting confirmation (the usual policy). Nothing is created yet; a person opens approve_url in Muvia and confirms. The change is then applied as that person.

{
  "jsonrpc": "2.0",
  "id": 11,
  "result": {
    "structuredContent": {
      "status": "pending",
      "action_id": "6ad9f4a04e5b8a1d7c0f2c31",
      "approve_url": "https://app.muviabi.com/approvals/6ad9f4a04e5b8a1d7c0f2c31?project=6abaad91a7a10614c38f70d5",
      "summary": "Create the dashboard \"Daily charge energy\" with 1 widget",
      "needs_admin": false
    },
    "isError": false
  }
}

Outcome 2: applied at once (the company's policy is «Run immediately»):

{
  "jsonrpc": "2.0",
  "id": 11,
  "result": {
    "structuredContent": {
      "status": "applied",
      "action_id": "6ad9f4a04e5b8a1d7c0f2c31",
      "id": "6ad9f4a04e5b8a1d7c0f2c32",
      "url": "https://app.muviabi.com/dashboards/6ad9f4a04e5b8a1d7c0f2c32?project=6abaad91a7a10614c38f70d5"
    },
    "isError": false
  }
}

A change made by a key is shown in Muvia as authored by «API key» and the key's name. create_analysis called with a key always answers with Outcome 1, whatever the policy: an analysis belongs to a person, so the person who confirms it creates and owns it (see MCP server). Through a person's OAuth assistant it answers in either shape, like create_dashboard.

14. What became of it: get_action

Ask later what a proposal turned into. status is pending, applied, discarded, stale, expired or failed; an applied action carries its result, a failed one a reason.

curl -s https://api.muviabi.com/mcp/v1 \
  -H "Authorization: Bearer $MUVIA_API_KEY" -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":8,"method":"tools/call","params":{"name":"get_action","arguments":{"action_id":"6ad9f3c24e5b8a1d7c0f2b19"}}}'
{
  "jsonrpc": "2.0",
  "id": 8,
  "result": {
    "structuredContent": {
      "status": "applied",
      "result": {
        "id": "6ad9f3c24e5b8a1d7c0f2b1a",
        "url": "https://app.muviabi.com/analysis?project=6abaad91a7a10614c38f70d5"
      }
    },
    "isError": false
  }
}

A key that may not write gets a refusal instead, with isError: true and the text «This API key can only read. To propose or make changes through an assistant, an administrator must turn on «Can write through assistants» on the key, in Settings › Company › API keys.» Some tools stay a person's even for a writing key; see MCP server. Do not retry a refusal unchanged.

Where next