# 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:

```bash
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.

```bash
curl -s -H "X-Api-Key: $MUVIA_API_KEY" https://api.muviabi.com/api/public/v1/whoami
```

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

```bash
curl -s -H "X-Api-Key: $MUVIA_API_KEY" https://api.muviabi.com/api/public/v1/company
```

```json
{ "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](/guides/mcp-server)).

### 3. List projects: GET /v1/projects

Needs `settings_read`.

```bash
curl -s -H "X-Api-Key: $MUVIA_API_KEY" "https://api.muviabi.com/api/public/v1/projects?size=2"
```

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

```bash
curl -s -H "X-Api-Key: $MUVIA_API_KEY" \
  "https://api.muviabi.com/api/public/v1/projects/6abaad91a7a10614c38f70d5/datasets?size=1"
```

```json
{
  "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/{project_id}/datasets/{dataset_id}/rows

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

```bash
curl -s -H "X-Api-Key: $MUVIA_API_KEY" \
  "https://api.muviabi.com/api/public/v1/projects/6abaad91a7a10614c38f70d5/datasets/6abab724a7a10614c38f731c/rows?size=2"
```

```json
{
  "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](/guides/rate-limits)). If anything fails, the answer is
the [error envelope](/guides/errors), 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:

```sql
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](/guides/mcp-server) for the model;
here is the wire exchange. MCP speaks JSON-RPC over HTTP POST. Every request
carries these headers:

```text
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

```bash
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"}}}'
```

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

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

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

```json
{
  "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](/mcp-tools) (as Markdown at `https://developers.muviabi.com/mcp-tools.md`).

### 9. Call whoami

```bash
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`.

```json
{
  "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](/guides/mcp-server#whoami).

### 10. Find a project: list_projects

```bash
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":{}}}'
```

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

```bash
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}}}'
```

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

```bash
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](/guides/mcp-server#writes)).

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

```json
{
  "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»):

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

```bash
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}}}'
```

```json
{
  "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).

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

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

```json
{
  "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»):

```json
{
  "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](/guides/mcp-server#writes)). 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`.

```bash
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"}}}'
```

```json
{
  "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](/guides/mcp-server#writes). Do not retry a refusal unchanged.

## Where next

- [MCP server](/guides/mcp-server): the model behind these calls.
- [MCP tools](/mcp-tools): every tool, its write mode and its arguments.
- [Errors](/guides/errors), [Rate limits](/guides/rate-limits), [Scopes](/guides/scopes).
- The [OpenAPI specification](/openapi.json) lists every REST operation.
