# mnrvAR API quickstart

Create, publish and measure WebAR experiences with an API key. This page is written for developers and for AI agents. The machine readable reference is the OpenAPI document at https://mnrvar.com/api/v1/openapi.json. A short index for agents is at https://mnrvar.com/llms.txt.

## Get an API key

1. Sign in at https://mnrvar.com.
2. Open Settings, then API keys.
3. Name the key, choose its scopes and select Create key.
4. Copy the key now. It is shown once. Keys look like `mnrv_live_` followed by a secret.

API keys need a paid plan, Creator or higher. A key acts as its owner and sees only that account's experiences. If the plan lapses, the key answers 402 `plan_required` until the account is upgraded again.

Send the key on every request:

```sh
export MNRV_API="https://mnrvar.com"
export MNRV_API_KEY="mnrv_live_REPLACE_WITH_YOUR_KEY"

curl -s "$MNRV_API/api/v1/templates" -H "Authorization: Bearer $MNRV_API_KEY"
```

## Scopes

Each key carries one or more scopes. A request needs the scope of its operation, shown as `x-required-scope` in the OpenAPI document. A key without it gets 403 `scope_missing`.

| Scope | Allows |
|---|---|
| `experiences` | Templates, creating, reading, editing, duplicating and deleting experiences, slots, objects, validation, folders, geo fence links |
| `publish` | Publishing and unpublishing |
| `assets` | Uploading files, importing files by URL, scoring target images, previewing and deleting draft assets |
| `analytics` | Scan, conversion, link, survey and geo analytics, and CSV exports |
| `leads` | Reading and exporting leads |

Keys cannot reach billing, team, admin, client portal, transfer or key management routes. Those answer 403 `endpoint_forbidden`.

## A complete session: video on a poster

This session builds a `video_on_surface` experience: a visitor scans a poster and a video plays on top of it. The template needs no visual editor. Use a key with the scopes `experiences`, `publish`, `assets` and `analytics`.

### 1. List templates

```sh
curl -s "$MNRV_API/api/v1/templates" -H "Authorization: Bearer $MNRV_API_KEY"
```

The response is a list. The entry for `video_on_surface`, trimmed:

```json
{
  "template_type": "video_on_surface",
  "description": "Plays an uploaded video on top of a scanned image, with optional transparency or a shape mask.",
  "slots": [
    { "slot_id": "target_image", "accepts": "image", "required": true, "accepted_extensions": [".gif", ".jpeg", ".jpg", ".png", ".webp"], "max_bytes": 5242880 },
    { "slot_id": "video", "accepts": "video", "required": true, "accepted_extensions": [".mov", ".mp4", ".webm"], "max_bytes": 104857600 },
    { "slot_id": "video_mask", "accepts": "image", "required": false, "accepted_extensions": [".gif", ".jpeg", ".jpg", ".png", ".webp"], "max_bytes": 5242880 }
  ],
  "plan_required": null,
  "addon_required": null,
  "editor_required": false
}
```

Only templates the account may create are listed. `editor_required: false` means the experience can be finished through the API alone.

### 2. Create the experience

```sh
curl -s -X POST "$MNRV_API/api/v1/experiences/" \
  -H "Authorization: Bearer $MNRV_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: spring-poster-create-1" \
  -d '{"name": "Spring poster", "template_type": "video_on_surface"}'
```

Response, trimmed:

```json
{
  "id": "3f2b8c1e-0d4a-4c8e-9a57-6d1f2b7c9e10",
  "name": "Spring poster",
  "template_type": "video_on_surface",
  "status": "draft",
  "hosted_url": null,
  "scan_count": 0
}
```

Keep the `id`:

```sh
export EXP_ID="3f2b8c1e-0d4a-4c8e-9a57-6d1f2b7c9e10"
```

### 3. Upload the poster

```sh
curl -s -X POST "$MNRV_API/api/v1/assets/upload?experience_id=$EXP_ID" \
  -H "Authorization: Bearer $MNRV_API_KEY" \
  -F "file=@poster.jpg"
```

Response:

```json
{
  "asset_id": "a1b2c3d4e5f6",
  "filename": "poster.jpg",
  "cdn_path": "drafts/3f2b8c1e-0d4a-4c8e-9a57-6d1f2b7c9e10/a1b2c3d4e5f6_poster.jpg",
  "size_bytes": 412345,
  "content_type": "image/jpeg"
}
```

```sh
export POSTER_PATH="drafts/3f2b8c1e-0d4a-4c8e-9a57-6d1f2b7c9e10/a1b2c3d4e5f6_poster.jpg"
```

To let the server download a public file instead of uploading it, use import by URL, see below.

### 4. Score the target image

A poster that is blurry, flat or repetitive tracks badly. Score it before you build on it.

```sh
curl -s -X POST "$MNRV_API/api/v1/assets/target-quality?cdn_path=$POSTER_PATH" \
  -H "Authorization: Bearer $MNRV_API_KEY"
```

Response, trimmed:

```json
{
  "score": 78,
  "grade": "good",
  "issues": [
    { "code": "large_blank_areas", "severity": "low" }
  ]
}
```

`grade` is `poor`, `fair`, `good` or `excellent`. On `poor`, ask for a different image. `issues` says what to improve.

### 5. Fill the target image slot

```sh
curl -s -X POST "$MNRV_API/api/v1/experiences/$EXP_ID/slots/target_image" \
  -H "Authorization: Bearer $MNRV_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"cdn_path\": \"$POSTER_PATH\"}"
```

Response:

```json
{
  "slot_id": "target_image",
  "component_type": "image",
  "asset_path": "drafts/3f2b8c1e-0d4a-4c8e-9a57-6d1f2b7c9e10/a1b2c3d4e5f6_poster.jpg",
  "filename": "poster.jpg",
  "created_object_id": null
}
```

### 6. Import the video by URL and fill the video slot

Import downloads a public https file, stores it as a draft asset and can fill a slot in the same call. Always send `video_aspect_ratio`, the video width divided by its height. The server cannot read video dimensions, and without it the video plane defaults to 16:9.

The server fits the video plane to the target image, so no manual resizing is needed. `fit` chooses how, and defaults to `contain`:

- `contain`: the whole video is visible, centred, and never larger than the target.
- `cover`: the video covers the whole target and overflows on one axis.
- `stretch`: the plane matches the target exactly and the video is distorted.

Setting or replacing `target_image` later re-fits the plane, so the order of the two calls does not matter.

```sh
curl -s -X POST "$MNRV_API/api/v1/assets/import-url" \
  -H "Authorization: Bearer $MNRV_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: spring-poster-video-1" \
  -d "{\"experience_id\": \"$EXP_ID\", \"url\": \"https://example.com/promo.mp4\", \"slot_id\": \"video\", \"video_aspect_ratio\": 1.7778, \"fit\": \"contain\"}"
```

Response, trimmed:

```json
{
  "filename": "promo.mp4",
  "cdn_path": "drafts/3f2b8c1e-0d4a-4c8e-9a57-6d1f2b7c9e10/f6e5d4c3b2a1_promo.mp4",
  "content_type": "video/mp4",
  "slot": {
    "slot_id": "video",
    "component_type": "video",
    "created_object_id": "7c0a5e52-9d3b-4f1a-8b66-2a4d9e1f3c58"
  }
}
```

`created_object_id` is the video plane the server created. Filling the video slot of a `video_on_surface` experience creates it for you. If you uploaded the video yourself, fill the slot with `POST /api/v1/experiences/{experience_id}/slots/{slot_id}` and send `video_aspect_ratio` and optionally `fit` in the body. The response then carries `video_plane_transform`, the size the plane was given in units of the target image height.

### 7. Validate

```sh
curl -s -X POST "$MNRV_API/api/v1/experiences/$EXP_ID/validate" \
  -H "Authorization: Bearer $MNRV_API_KEY"
```

Validation is a dry run of publish. It reports every blocking problem at once and writes nothing.

```json
{ "ok": true, "problems": [] }
```

With a slot still empty it returns, for example:

```json
{
  "ok": false,
  "problems": [
    { "code": "slot_missing", "message": "Required slot not filled: video", "field": "video" }
  ]
}
```

Fix every problem, then validate again. A problem with an `upgrade_url` needs the account owner to change plan.

### 8. Publish

```sh
curl -s -X POST "$MNRV_API/api/v1/experiences/$EXP_ID/publish" \
  -H "Authorization: Bearer $MNRV_API_KEY" \
  -H "Idempotency-Key: spring-poster-publish-1"
```

Response, trimmed:

```json
{
  "id": "3f2b8c1e-0d4a-4c8e-9a57-6d1f2b7c9e10",
  "status": "live",
  "hosted_url": "https://cdn.mnrvar.com/experiences/3f2b8c1e-0d4a-4c8e-9a57-6d1f2b7c9e10/index.html"
}
```

Give `hosted_url` to the person who will scan. Publishing can answer 402 with a `publish_limit` or `addon_required` code and an `upgrade_url` when the plan has no room. Tell the account owner to open that URL. To take an experience offline, call `POST /api/v1/experiences/{experience_id}/unpublish`. That removes the published files and returns the experience to draft, and publishing again makes it live.

### 9. Read results later

```sh
curl -s "$MNRV_API/api/v1/analytics/summary?experience_id=$EXP_ID&period=7d" \
  -H "Authorization: Bearer $MNRV_API_KEY"

curl -s "$MNRV_API/api/v1/leads?experience_id=$EXP_ID&period=30d&limit=50" \
  -H "Authorization: Bearer $MNRV_API_KEY"
```

The summary returns `total_scans`, `unique_sessions`, a daily `timeline` of `{date, count}` and a `by_experience` breakdown. Leads return `{leads, total}`. Page with `limit` and `offset`. `GET /api/v1/analytics/export` and `GET /api/v1/leads/export` return CSV files.

## Other templates without the editor

| Template | Needs |
|---|---|
| `video_on_surface` | A target image and a video. This session. |
| `product_float` | A target image and one 3D model, added with `POST /api/v1/experiences/{experience_id}/objects`. The model gets the default transform. |
| `vr_360_video` | One 360 video in the `video` slot. Needs the Studio plan and the 360 video add-on. |

`GET /api/v1/templates` is the source of truth. Check `editor_required` on each template.

## Errors

Every 4xx response is JSON. Branch on `code`, show `detail` to people.

```json
{
  "detail": "You have used all of your published experience slots. Upgrade to Creator, Studio or Agency, or add a slot from Billing.",
  "code": "publish_limit",
  "upgrade_url": "https://mnrvar.com/billing"
}
```

The wording of `detail` varies and the example above is illustrative. `field` names the offending input when there is one. `upgrade_url` appears on 402. Request validation failures keep a list in `detail` and add `code` `invalid_request`, a `message` string and `field`.

| Code | Status | Meaning | What to do |
|---|---|---|---|
| `invalid_api_key` | 401 | The key is missing, malformed, revoked or unknown. | Check the `Authorization: Bearer` header. Create a new key if it was revoked. |
| `scope_missing` | 403 | The key lacks the scope this operation needs. `field` names it. | Create a key with that scope. |
| `endpoint_forbidden` | 403 | API keys cannot call this route. | Use only operations from the OpenAPI document. |
| `team_member_key` | 403 | Team members cannot use API keys. | Use a key owned by the account owner. |
| `rate_limited` | 429 | Too many requests. | Wait the seconds in the `Retry-After` header, then retry. |
| `plan_required` | 402 | The plan does not include this feature, including API access itself. | Give the user the `upgrade_url`. |
| `addon_required` | 402 | The template needs an add-on slot. | Give the user the `upgrade_url`. |
| `publish_limit` | 402 | The plan has no room for another published experience. | Unpublish one, or give the user the `upgrade_url`. |
| `create_limit` | 402 | The plan has no room for another experience. | Delete one, or give the user the `upgrade_url`. |
| `template_unavailable` | 403 | The template is not available to this account. | Choose another template from `GET /api/v1/templates`. |
| `invalid_request` | 422 | A field is missing, malformed or out of range. | Fix the input named in `field`. |
| `not_found` | 404 | The experience, folder or other resource does not exist for this account. | Check the id. |
| `slot_unknown` | 422 | The template has no slot with that id. | Use a `slot_id` from `GET /api/v1/templates`. |
| `slot_missing` | 422 | A required slot is empty, so the experience cannot publish. | Fill the slot named in `field`. |
| `no_objects` | 422 | The template needs at least one 3D model. | Add one with `POST /api/v1/experiences/{experience_id}/objects`. |
| `asset_invalid` | 400, 413 or 422 | The file or path is not acceptable: type, size or location. | Check extension and size limits from the template. Use a `cdn_path` from your own upload. |
| `asset_missing` | 404 or 422 | A referenced file is not in storage. | Upload or import the file again. |
| `capability_violation` | 422 | The config uses a feature the template does not support. | Remove that setting. |
| `schedule_ended` | 422 | The experience's `ends_at` is in the past. | Change or clear `ends_at`, then publish. |
| `url_not_allowed` | 422 | The import URL is not a public https URL. | See import by URL below. |
| `download_failed` | 422 | The server could not download the file. | Check the URL is public and serves the file. |
| `idempotency_key_reused` | 422 | The `Idempotency-Key` was used with a different request. | Use a new key for a new request. |
| `idempotency_in_progress` | 409 | A request with this key is still running. | Retry after `Retry-After` seconds. |
| `key_limit` | 422 | The account already has 10 active keys. | Revoke one. |

## Limits

- Rate limit: 120 requests per minute per key. Past it the API answers 429 `rate_limited` with `Retry-After`.
- Import by URL: 30 imports per 10 minutes per account, also answered with 429 `rate_limited`.
- File size and type limits are per slot in `GET /api/v1/templates`.
- Plan quotas for experiences, published experiences and add-ons answer 402 with an `upgrade_url`.
- Scopes `experiences`, `publish`, `assets`, `analytics` and `leads` are the only ones.

## Idempotency

Create, publish, add object and import by URL accept an `Idempotency-Key` header, 1 to 255 visible ASCII characters. Repeat the same request with the same key within 24 hours and you get the first response back, with `Idempotent-Replayed: true`. No second experience is created and nothing is published twice. Use one key per intended action. Send a different request with a used key and you get 422 `idempotency_key_reused`. If the first request is still running you get 409 `idempotency_in_progress`.

## Import by URL

`POST /api/v1/assets/import-url` makes the server download a file, so it is strict:

- https only, default port, no credentials in the URL.
- The host must resolve to a public address. Private, loopback and link local addresses are refused with 422 `url_not_allowed`.
- At most 3 redirects, each checked again.
- The file type comes from the filename in `Content-Disposition`, else the last segment of the URL path. It must be an extension the template accepts, within the size limit.
- 30 imports per 10 minutes per account.

## What still needs the visual editor

The API fills template slots and adds 3D models with a default transform. It does not place or arrange a scene. Open the editor at https://mnrvar.com for:

- Templates marked `editor_required: true` in `GET /api/v1/templates`.
- Positioning, rotating and scaling models precisely.
- Call to action buttons, lead forms, hotspots, timelines and animations.
- Catalog items and `magic_cube` faces.
- Creating geo fences, which the API can only link to an experience.

You can still create and publish those experiences through the API, but a person should check the result in the editor first.

## Reference

- OpenAPI document: https://mnrvar.com/api/v1/openapi.json
- Agent index: https://mnrvar.com/llms.txt
- This page as markdown: https://mnrvar.com/docs/api.md
