---
title: "API reference"
description: "Every operation of the API: method, path, authentication, parameters, request body, responses, with an example of each. Generated from the OpenAPI contract."
source_url: https://spacecontext.app/developers/docs/api/reference/
---

# API reference

Every operation of the Space Context API v1: parameters, request bodies, responses and an example of each. Generated from the OpenAPI contract.

Base URL `https://api.spacecontext.app/v1`. JSON in `snake_case`, timestamps ISO 8601 (UTC), money in US cents. Authenticate with `Authorization: Bearer sc_live_…` (an API key) or, for the console operations, a console session. Errors come as `{ "error": { "code", "message", "request_id" } }`.

Machine-readable contract: https://spacecontext.app/developers/docs/openapi.json · typed client: `@spacecontext/sdk` (https://github.com/finikoslab/spacecontext).

## Spaces

Spaces and their documents: create, list, read, search within, ask, export, compare, delete.

### GET /spaces

**List the spaces this key can read: scanned with the app (live keys) and created through the API**

- Auth: API key (`Authorization: Bearer sc_…`)
- Operation id: `listSpaces`

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `limit` | query | integer (≥ 1, ≤ 50) | no |  |
| `cursor` | query | string | no |  |
| `source` | query | string: "app" \| "api" | no | Only spaces from this source. Omitted: every space this key can read (app spaces need a live key; test keys see the demo space). |

**Responses**

- `200` — Spaces page (SpaceListResponse)
- `400` — Error response (Error)
- `401` — Error response (Error)
- `403` — Error response (Error)
- `429` — Too many requests. Retry after the number of seconds in `Retry-After`. (Error)

Example `200` response:

```json
{
  "data": [
    {
      "object": "space",
      "id": "spc_3f9a…",
      "source": "app",
      "name": "Seaside flat",
      "space_type": "apartment",
      "created_at": "2026-10-05T13:54:37.000Z",
      "updated_at": "2026-10-05T13:57:02.000Z",
      "latest_scan": {
        "id": "spc_3f9a…",
        "analyzed_at": "2026-10-05T13:57:02.000Z",
        "areas": 1,
        "items": 1,
        "issues": 1
      }
    }
  ],
  "next_cursor": "string"
}
```

### POST /spaces

**Create a space to scan through the API**

Creates a space in the key's mode (test or live). With an `external_id` already used in that mode, returns the existing space with 200 instead of creating another. An account creates at most 200 spaces a day in each mode (by default, test keys included; a replay does not count): past that, 429 `RATE_LIMIT_EXCEEDED` with `Retry-After` (seconds to midnight UTC). Needs `spaces:read` and `scans:write`.

- Auth: API key (`Authorization: Bearer sc_…`)
- Operation id: `createSpace`

**Request body** (`application/json`, required): SpaceCreateRequest

```json
{
  "name": "Seaside flat",
  "description": "…",
  "space_type": "apartment",
  "external_id": "ext_…"
}
```

**Responses**

- `200` — The space with this external_id (ApiSpace)
- `201` — Space created (ApiSpace)
- `400` — Error response (Error)
- `401` — Error response (Error)
- `403` — Error response (Error)
- `429` — Too many requests. Retry after the number of seconds in `Retry-After`. (Error)

Example `200` response:

```json
{
  "object": "space",
  "id": "spc_3f9a…",
  "source": "api",
  "livemode": true,
  "name": "Seaside flat",
  "description": "…",
  "space_type": "string",
  "external_id": "ext_…",
  "created_at": "2026-10-05T13:54:37.000Z",
  "updated_at": "2026-10-05T13:57:02.000Z"
}
```

### GET /spaces/{id}

**Get the current document for a space**

Keys without the `spaces:sensitive` scope only read what the deep pass has screened for sensitive values. Until it has (`enrichment.level` is `base`), the document has the same shape with its content withheld: the lists are empty and `stats` gives the counts. Once `enrichment.level` is `deep`, sensitive facts are redacted, their values are removed from every other text, and images are given only for the frames that non-sensitive records cite as evidence. Fields the deep pass does not read (project description, summary, area layout) and raw first-hand sources (transcript, owner answers, narrator quotes) always require the scope.

- Auth: API key (`Authorization: Bearer sc_…`)
- Operation id: `getSpace`

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string (pattern ^(app\|spc)_) | yes |  |
| `include` | query | string: "transcript" | no | Adds the transcript. Requires the `spaces:sensitive` scope; without it the request is refused with 403 `INSUFFICIENT_SCOPE`. |
| `enrich` | query | string: "auto" \| "none" | no |  |

**Responses**

- `200` — Space document (SpaceDocument)
- `400` — Error response (Error)
- `401` — Error response (Error)
- `403` — Error response (Error)
- `404` — Error response (Error)
- `429` — Too many requests. Retry after the number of seconds in `Retry-After`. (Error)

Example `200` response:

```json
{
  "object": "space_document",
  "schema_version": "1.0",
  "space": {
    "id": "spc_3f9a…",
    "source": "app",
    "name": "Seaside flat",
    "description": "…",
    "space_type": "apartment",
    "created_at": "2026-10-05T13:54:37.000Z",
    "updated_at": "2026-10-05T13:57:02.000Z"
  },
  "scan": {
    "id": "spc_3f9a…",
    "analyzed_at": "2026-10-05T13:57:02.000Z",
    "recorded_at": "2026-10-05T13:57:02.000Z",
    "duration_seconds": 1,
    "device": "string",
    "video_sha256": "string",
    "frame_count": 1
  },
  "enrichment": {
    "level": "base",
    "status": "not_requested",
    "updated_at": "2026-10-05T13:57:02.000Z"
  },
  "summary": "…",
  "confidence": 0,
  "stats": {
    "areas": 1,
    "items": 1,
    "issues": 1,
    "key_locations": 1,
    "instructions": 1,
    "facts": 1
  },
  "features": [
    "string"
  ],
  "areas": [
    {
      "id": "spc_3f9a…",
      "name": "Seaside flat",
      "type": "entrance",
      "level": "string",
      "condition": {
        "code": "new",
        "label": "…",
        "notes": "string"
      },
      "description": "…",
      "layout": "string",
      "orientation": "string",
      "features": [
        "string"
      ],
      "access_from": [
        "string"
      ],
      "item_ids": [
        "string"
      ],
      "issue_ids": [
        "string"
      ],
      "evidence": [
        {
          "frame_id": "fra_…",
          "timestamp_ms": 1200
        }
      ],
      "confidence": 0
    }
  ],
  "items": [
    {
      "id": "spc_3f9a…",
      "area_id": "are_…",
      "name": "Seaside flat",
      "canonical_name": "string",
      "description": "…",
      "category": "furniture",
      "subcategory": "string",
      "quantity": 1,
      "location": "string",
      "placement": {
        "anchor": "string",
        "anchor_item_id": "anc_…",
        "relation": "on",
        "viewpoint": "string",
        "detail": "string"
      },
      "condition": {
        "code": "new",
        "label": "…",
        "notes": "string"
      },
      "brand": "string",
      "model": "string",
      "attributes": [
        "string"
      ],
      "visible_text": [
        "string"
      ],
      "aliases": [
        "string"
      ],
      "note": "string",
      "issue_ids": [
        "string"
      ],
      "previous_item_id": "pre_…",
      "evidence": [
        {
          "frame_id": "fra_…",
          "timestamp_ms": 1200
        }
      ],
      "verification": {
        "status": "verified",
        "probability": 0,
        "frame_id": "fra_…"
      },
      "confidence": 0
    }
  ],
  "issues": [
    {
      "id": "spc_3f9a…",
      "area_id": "are_…",
      "item_id": "ite_…",
      "title": "…",
      "description": "…",
      "category": "damage",
      "severity": "low",
      "suggested_action": "string",
      "previous_issue_id": "pre_…",
      "evidence": [
        {
          "frame_id": "fra_…",
          "timestamp_ms": 1200
        }
      ],
      "verification": {
        "status": "verified",
        "probability": 0,
        "frame_id": "fra_…"
      },
      "confidence": 0
    }
  ],
  "key_locations": [
    {
      "id": "spc_3f9a…",
      "kind": "electrical_panel",
      "label": "…",
      "description": "…",
      "area_id": "are_…",
      "location": "string",
      "placement": {
        "anchor": "string",
        "anchor_item_id": "anc_…",
        "relation": "on",
        "viewpoint": "string",
        "detail": "string"
      },
      "notes": "string",
      "evidence": [
        {
          "frame_id": "fra_…",
          "timestamp_ms": 1200
        }
      ],
      "verification": {
        "status": "verified",
        "probability": 0,
        "frame_id": "fra_…"
      },
      "confidence": 0
    }
  ],
  "instructions": [
    {
      "id": "spc_3f9a…",
      "title": "…",
      "area_id": "are_…",
      "item_id": "ite_…",
      "steps": [
        "string"
      ],
      "source_quote": "string",
      "transcript_segment_ids": [
        "string"
      ],
      "confidence": 0
    }
  ],
  "facts": [
    {
      "id": "spc_3f9a…",
      "kind": "access",
      "statement": "…",
      "value": "string",
      "area_id": "are_…",
      "sensitive": false,
      "redacted": false,
      "transcript_segment_ids": [
        "string"
      ],
      "evidence": [
        {
          "frame_id": "fra_…",
          "timestamp_ms": 1200
        }
      ],
      "confidence": 0
    }
  ],
  "qa": [
    {
      "id": "spc_3f9a…",
      "question": "…",
      "answer": "…",
      "source": "owner",
      "category": "string",
      "area_id": "are_…"
    }
  ],
  "transcript": {
    "text": "…",
    "segments": [
      {
        "id": "spc_3f9a…",
        "start_ms": 1200,
        "end_ms": 1200,
        "text": "…"
      }
    ]
  },
  "frames": [
    {
      "id": "spc_3f9a…",
      "index": 1,
      "timestamp_ms": 1200,
      "url": "https://…",
      "url_expires_at": "2026-10-05T13:57:02.000Z"
    }
  ],
  "continuity": {
    "previous_scan_id": "pre_…",
    "absent_items": [
      {
        "item_id": "ite_…",
        "name": "Seaside flat",
        "note": "string",
        "evidence": [
          {
            "frame_id": null,
            "timestamp_ms": null
          }
        ]
      }
    ],
    "resolved_issues": [
      {
        "issue_id": "iss_…",
        "title": "…",
        "note": "string",
        "evidence": [
          {
            "frame_id": null,
            "timestamp_ms": null
          }
        ]
      }
    ]
  },
  "coverage": {
    "warnings": [
      "string"
    ],
    "not_documented": [
      "string"
    ],
    "checklist": [
      {
        "kind": "electrical_panel",
        "seen": true,
        "probability": 0,
        "evidence": [
          {
            "frame_id": "fra_…",
            "timestamp_ms": 1200
          }
        ]
      }
    ]
  }
}
```

### DELETE /spaces/{id}

**Delete a space created through the API, with its scans and files**

Not possible while one of its scans is processing (409). App spaces are deleted in the app. Needs `scans:write`.

- Auth: API key (`Authorization: Bearer sc_…`)
- Operation id: `deleteSpace`

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string (pattern ^(app\|spc)_) | yes |  |

**Responses**

- `204` — Deleted
- `401` — Error response (Error)
- `403` — Error response (Error)
- `404` — Error response (Error)
- `409` — Error response (Error)
- `429` — Too many requests. Retry after the number of seconds in `Retry-After`. (Error)

### GET /spaces/{id}/scans

**List scans for a space**

- Auth: API key (`Authorization: Bearer sc_…`)
- Operation id: `listSpaceScans`

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string (pattern ^(app\|spc)_) | yes |  |

**Responses**

- `200` — Scan summaries (ScanListResponse)
- `401` — Error response (Error)
- `403` — Error response (Error)
- `404` — Error response (Error)
- `429` — Too many requests. Retry after the number of seconds in `Retry-After`. (Error)

Example `200` response:

```json
{
  "data": [
    {
      "object": "scan",
      "id": "spc_3f9a…",
      "space_id": "spc_3f9a…",
      "analyzed_at": "2026-10-05T13:57:02.000Z",
      "areas": 1,
      "items": 1,
      "issues": 1,
      "current": true
    }
  ]
}
```

### POST /spaces/{id}/scans

**Create a scan of a space created through the API**

Declares what will be uploaded (frames with their timestamps, or one video), the language of the report (`en`, `it`, `es`, `fr`, `de`, `pt`, `nl`; the narration can be in any language) and the optional transcript. The response says how to upload in `upload`; then call `POST /scans/{id}/start`. A live scan needs a card on file, asked here, before any upload link: 402 `PAYMENT_METHOD_REQUIRED` (added in the console, nothing charged), 503 `PAYMENTS_UNAVAILABLE` where payments are not configured; the same rule as the start. A test scan never needs one; its uploads go through the API (a video of at most 95 MiB) and are counted, never stored. The same `Idempotency-Key` returns the same scan. An account creates at most 500 scans a day in each mode (by default, test keys included; a replay does not count): past that, 429 `RATE_LIMIT_EXCEEDED` with `Retry-After` (seconds to midnight UTC). An account holds at most 20 scans created and not started (both modes): past that, 429 `SCANS_IN_PROGRESS` (`max_open_scans`) until some are started or cancelled. A test scan keeps only whether it had a transcript, never its text (its result is the demo). Needs `scans:write`.

- Auth: API key (`Authorization: Bearer sc_…`)
- Operation id: `createScan`

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string (pattern ^(app\|spc)_) | yes |  |
| `Idempotency-Key` | header | string (pattern ^[A-Za-z0-9_-]{8,64}$) | no |  |

**Request body** (`application/json`, required): ScanCreateRequest

```json
{
  "input": {
    "kind": "frames",
    "language": "en",
    "frames": [
      {
        "timestamp_ms": 1200
      }
    ],
    "video": {
      "content_type": "video/mp4",
      "content_length": 1
    },
    "transcript": {
      "text": "…",
      "segments": [
        {
          "start_ms": 1200,
          "end_ms": 1200,
          "text": "…"
        }
      ]
    },
    "recorded_at": "2026-10-05T13:57:02.000Z",
    "device": "string",
    "duration_seconds": 1
  }
}
```

**Responses**

- `200` — The scan created with this Idempotency-Key (Scan)
- `201` — Scan created (Scan)
- `400` — Error response (Error)
- `401` — Error response (Error)
- `402` — Error response (Error)
- `403` — Error response (Error)
- `404` — Error response (Error)
- `409` — Error response (Error)
- `429` — Too many requests. Retry after the number of seconds in `Retry-After`. (Error)
- `503` — Error response (Error)

Example `200` response:

```json
{
  "object": "scan",
  "id": "spc_3f9a…",
  "space_id": "spc_3f9a…",
  "livemode": true,
  "status": "awaiting_upload",
  "stage": "ingesting",
  "input": {
    "kind": "frames",
    "language": "en",
    "frame_count": 1,
    "video": {
      "content_type": "string",
      "content_length": 1
    },
    "has_transcript": true
  },
  "charge": {
    "kind": "free_scan",
    "price_cents": 100,
    "refunded": false
  },
  "error": {
    "code": "string",
    "message": "string"
  },
  "upload": {
    "kind": "frames",
    "method": "PUT",
    "url": "https://…",
    "url_template": "string",
    "headers": {},
    "frame_count": 1,
    "max_bytes": 1,
    "content_types": [
      "string"
    ],
    "expires_at": "2026-10-05T13:57:02.000Z"
  },
  "created_at": "2026-10-05T13:54:37.000Z",
  "started_at": "2026-10-05T13:54:40.000Z",
  "completed_at": "2026-10-05T13:57:02.000Z"
}
```

### GET /spaces/{id}/scans/{scan_id}

**Get a base document for a specific scan**

A scan read by id is the snapshot as it was recorded. Past scans are not screened for sensitive values, so this operation requires the `spaces:sensitive` scope and answers 403 `INSUFFICIENT_SCOPE` without it.

- Auth: API key (`Authorization: Bearer sc_…`)
- Operation id: `getSpaceScan`

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string (pattern ^(app\|spc)_) | yes |  |
| `scan_id` | path | string (pattern ^scn_) | yes |  |
| `include` | query | string: "transcript" | no | Adds the transcript. Requires the `spaces:sensitive` scope; without it the request is refused with 403 `INSUFFICIENT_SCOPE`. |

**Responses**

- `200` — Space document for the scan (SpaceDocument)
- `400` — Error response (Error)
- `401` — Error response (Error)
- `403` — Error response (Error)
- `404` — Error response (Error)
- `429` — Too many requests. Retry after the number of seconds in `Retry-After`. (Error)

Example `200` response:

```json
{
  "object": "space_document",
  "schema_version": "1.0",
  "space": {
    "id": "spc_3f9a…",
    "source": "app",
    "name": "Seaside flat",
    "description": "…",
    "space_type": "apartment",
    "created_at": "2026-10-05T13:54:37.000Z",
    "updated_at": "2026-10-05T13:57:02.000Z"
  },
  "scan": {
    "id": "spc_3f9a…",
    "analyzed_at": "2026-10-05T13:57:02.000Z",
    "recorded_at": "2026-10-05T13:57:02.000Z",
    "duration_seconds": 1,
    "device": "string",
    "video_sha256": "string",
    "frame_count": 1
  },
  "enrichment": {
    "level": "base",
    "status": "not_requested",
    "updated_at": "2026-10-05T13:57:02.000Z"
  },
  "summary": "…",
  "confidence": 0,
  "stats": {
    "areas": 1,
    "items": 1,
    "issues": 1,
    "key_locations": 1,
    "instructions": 1,
    "facts": 1
  },
  "features": [
    "string"
  ],
  "areas": [
    {
      "id": "spc_3f9a…",
      "name": "Seaside flat",
      "type": "entrance",
      "level": "string",
      "condition": {
        "code": "new",
        "label": "…",
        "notes": "string"
      },
      "description": "…",
      "layout": "string",
      "orientation": "string",
      "features": [
        "string"
      ],
      "access_from": [
        "string"
      ],
      "item_ids": [
        "string"
      ],
      "issue_ids": [
        "string"
      ],
      "evidence": [
        {
          "frame_id": "fra_…",
          "timestamp_ms": 1200
        }
      ],
      "confidence": 0
    }
  ],
  "items": [
    {
      "id": "spc_3f9a…",
      "area_id": "are_…",
      "name": "Seaside flat",
      "canonical_name": "string",
      "description": "…",
      "category": "furniture",
      "subcategory": "string",
      "quantity": 1,
      "location": "string",
      "placement": {
        "anchor": "string",
        "anchor_item_id": "anc_…",
        "relation": "on",
        "viewpoint": "string",
        "detail": "string"
      },
      "condition": {
        "code": "new",
        "label": "…",
        "notes": "string"
      },
      "brand": "string",
      "model": "string",
      "attributes": [
        "string"
      ],
      "visible_text": [
        "string"
      ],
      "aliases": [
        "string"
      ],
      "note": "string",
      "issue_ids": [
        "string"
      ],
      "previous_item_id": "pre_…",
      "evidence": [
        {
          "frame_id": "fra_…",
          "timestamp_ms": 1200
        }
      ],
      "verification": {
        "status": "verified",
        "probability": 0,
        "frame_id": "fra_…"
      },
      "confidence": 0
    }
  ],
  "issues": [
    {
      "id": "spc_3f9a…",
      "area_id": "are_…",
      "item_id": "ite_…",
      "title": "…",
      "description": "…",
      "category": "damage",
      "severity": "low",
      "suggested_action": "string",
      "previous_issue_id": "pre_…",
      "evidence": [
        {
          "frame_id": "fra_…",
          "timestamp_ms": 1200
        }
      ],
      "verification": {
        "status": "verified",
        "probability": 0,
        "frame_id": "fra_…"
      },
      "confidence": 0
    }
  ],
  "key_locations": [
    {
      "id": "spc_3f9a…",
      "kind": "electrical_panel",
      "label": "…",
      "description": "…",
      "area_id": "are_…",
      "location": "string",
      "placement": {
        "anchor": "string",
        "anchor_item_id": "anc_…",
        "relation": "on",
        "viewpoint": "string",
        "detail": "string"
      },
      "notes": "string",
      "evidence": [
        {
          "frame_id": "fra_…",
          "timestamp_ms": 1200
        }
      ],
      "verification": {
        "status": "verified",
        "probability": 0,
        "frame_id": "fra_…"
      },
      "confidence": 0
    }
  ],
  "instructions": [
    {
      "id": "spc_3f9a…",
      "title": "…",
      "area_id": "are_…",
      "item_id": "ite_…",
      "steps": [
        "string"
      ],
      "source_quote": "string",
      "transcript_segment_ids": [
        "string"
      ],
      "confidence": 0
    }
  ],
  "facts": [
    {
      "id": "spc_3f9a…",
      "kind": "access",
      "statement": "…",
      "value": "string",
      "area_id": "are_…",
      "sensitive": false,
      "redacted": false,
      "transcript_segment_ids": [
        "string"
      ],
      "evidence": [
        {
          "frame_id": "fra_…",
          "timestamp_ms": 1200
        }
      ],
      "confidence": 0
    }
  ],
  "qa": [
    {
      "id": "spc_3f9a…",
      "question": "…",
      "answer": "…",
      "source": "owner",
      "category": "string",
      "area_id": "are_…"
    }
  ],
  "transcript": {
    "text": "…",
    "segments": [
      {
        "id": "spc_3f9a…",
        "start_ms": 1200,
        "end_ms": 1200,
        "text": "…"
      }
    ]
  },
  "frames": [
    {
      "id": "spc_3f9a…",
      "index": 1,
      "timestamp_ms": 1200,
      "url": "https://…",
      "url_expires_at": "2026-10-05T13:57:02.000Z"
    }
  ],
  "continuity": {
    "previous_scan_id": "pre_…",
    "absent_items": [
      {
        "item_id": "ite_…",
        "name": "Seaside flat",
        "note": "string",
        "evidence": [
          {
            "frame_id": null,
            "timestamp_ms": null
          }
        ]
      }
    ],
    "resolved_issues": [
      {
        "issue_id": "iss_…",
        "title": "…",
        "note": "string",
        "evidence": [
          {
            "frame_id": null,
            "timestamp_ms": null
          }
        ]
      }
    ]
  },
  "coverage": {
    "warnings": [
      "string"
    ],
    "not_documented": [
      "string"
    ],
    "checklist": [
      {
        "kind": "electrical_panel",
        "seen": true,
        "probability": 0,
        "evidence": [
          {
            "frame_id": "fra_…",
            "timestamp_ms": 1200
          }
        ]
      }
    ]
  }
}
```

### GET /spaces/{id}/skill

**Export a space as a multi-file Agent Skill**

Returns the files of a ready-to-use skill built from the current document, exactly as this key may read it: a redacted document gives a redacted skill. `SKILL.md` is a short static guide; the knowledge is in `references/` and `space.json`, which an agent opens only when needed. A key without `spaces:sensitive` gets 409 `ENRICHMENT_PENDING` until the deep pass has screened the analysis.

- Auth: API key (`Authorization: Bearer sc_…`)
- Operation id: `getSpaceSkill`

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string (pattern ^(app\|spc)_) | yes |  |
| `include` | query | string: "transcript" | no | Adds `references/transcript.md`. Requires the `spaces:sensitive` scope; without it the request is refused with 403 `INSUFFICIENT_SCOPE`. |
| `enrich` | query | string: "auto" \| "none" | no |  |

**Responses**

- `200` — Skill bundle (SkillBundle)
- `400` — Error response (Error)
- `401` — Error response (Error)
- `403` — Error response (Error)
- `404` — Error response (Error)
- `409` — Error response (Error)
- `429` — Too many requests. Retry after the number of seconds in `Retry-After`. (Error)

Example `200` response:

```json
{
  "object": "skill",
  "name": "Seaside flat",
  "space_id": "spc_3f9a…",
  "scan_id": "scn_8a21…",
  "files": [
    {
      "path": "string",
      "media_type": "text/markdown",
      "content": "string"
    }
  ]
}
```

### GET /spaces/{id}/markdown

**Export a space as one Markdown page**

The current document as a single readable page (`text/markdown`): summary, every area with its items, issues, key locations and how-to steps, then facts, Q&A, coverage and the recording. Built from exactly the document this key may read, so a redacted document gives a redacted page; image links are never included. The same 409 `ENRICHMENT_PENDING` rule as the Skill export applies to keys without `spaces:sensitive`.

- Auth: API key (`Authorization: Bearer sc_…`)
- Operation id: `getSpaceMarkdown`

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string (pattern ^(app\|spc)_) | yes |  |
| `include` | query | string: "transcript" | no | Adds the transcript. Requires the `spaces:sensitive` scope; without it the request is refused with 403 `INSUFFICIENT_SCOPE`. |
| `enrich` | query | string: "auto" \| "none" | no |  |

**Responses**

- `200` — Markdown page (text/markdown)
- `400` — Error response (Error)
- `401` — Error response (Error)
- `403` — Error response (Error)
- `404` — Error response (Error)
- `409` — Error response (Error)
- `429` — Too many requests. Retry after the number of seconds in `Retry-After`. (Error)

### POST /spaces/{id}/ask

**Ask a question about a space**

Answers from the records of the current document this key may read, in one to three sentences, with the records it relies on and their evidence images. Says so when the answer is not documented, and never reveals a redacted value. Test keys get the best-matching record of the demo, without a model. A key without `spaces:sensitive` gets 409 `ENRICHMENT_PENDING` until the space is screened. Limits: 20 a minute and, by default, 200 a day per account.

- Auth: API key (`Authorization: Bearer sc_…`)
- Operation id: `askSpace`

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string (pattern ^(app\|spc)_) | yes |  |

**Request body** (`application/json`, required): object

```json
{
  "question": "…"
}
```

**Responses**

- `200` — Answer (SpaceAnswer)
- `400` — Error response (Error)
- `401` — Error response (Error)
- `403` — Error response (Error)
- `404` — Error response (Error)
- `409` — Error response (Error)
- `429` — Too many requests. Retry after the number of seconds in `Retry-After`. (Error)

Example `200` response:

```json
{
  "object": "answer",
  "space_id": "spc_3f9a…",
  "scan_id": "scn_8a21…",
  "question": "…",
  "answer": "…",
  "documented": true,
  "citations": [
    {
      "record_type": "area",
      "record_id": "rec_…",
      "title": "…",
      "area": "string",
      "evidence": [
        {
          "frame_id": "fra_…",
          "timestamp_ms": 1200,
          "url": "https://…"
        }
      ]
    }
  ]
}
```

### GET /spaces/{id}/diff

**Compare two scans of a space**

What changed from the scan `from` to the scan `to` (the current one when omitted): areas, items added, removed, not seen, moved or changed, issues new, resolved or not seen. Computed without a model. When `to` was checked against `from` (consecutive scans of a space created through the API) records are matched by the links it made, and removals are confirmed by a frame of the empty place; otherwise records are matched by content (name, generic name, aliases) and what is missing is reported as `not_seen`. For a space scanned with the app, past scans are not screened for sensitive values, so this operation requires the `spaces:sensitive` scope.

- Auth: API key (`Authorization: Bearer sc_…`)
- Operation id: `getSpaceDiff`

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string (pattern ^(app\|spc)_) | yes |  |
| `from` | query | string (pattern ^scn_) | yes | Id of the earlier scan. |
| `to` | query | string (pattern ^scn_) | no | Id of the later scan. Defaults to the current one. |

**Responses**

- `200` — Differences between the two scans (SpaceDiff)
- `400` — Error response (Error)
- `401` — Error response (Error)
- `403` — Error response (Error)
- `404` — Error response (Error)
- `429` — Too many requests. Retry after the number of seconds in `Retry-After`. (Error)

Example `200` response:

```json
{
  "object": "space_diff",
  "space_id": "spc_3f9a…",
  "from": {
    "scan_id": "scn_8a21…",
    "analyzed_at": "2026-10-05T13:57:02.000Z"
  },
  "to": {
    "scan_id": "scn_8a21…",
    "analyzed_at": "2026-10-05T13:57:02.000Z"
  },
  "level": "base",
  "linked": true,
  "summary": {
    "areas_added": 0,
    "areas_removed": 0,
    "items_added": 0,
    "items_removed": 0,
    "items_not_seen": 0,
    "items_moved": 0,
    "items_changed": 0,
    "issues_new": 0,
    "issues_resolved": 0,
    "issues_not_seen": 0
  },
  "areas": {
    "added": [
      "string"
    ],
    "removed": [
      "string"
    ],
    "condition_changed": [
      {
        "area": "string",
        "from": "new",
        "to": "new"
      }
    ]
  },
  "items": {
    "added": [
      {
        "name": "Seaside flat",
        "canonical_name": "string",
        "area": "string",
        "quantity": 1,
        "evidence": {
          "from": [
            {
              "frame_id": null,
              "timestamp_ms": null,
              "url": null
            }
          ],
          "to": [
            {
              "frame_id": null,
              "timestamp_ms": null,
              "url": null
            }
          ]
        }
      }
    ],
    "removed": [
      {
        "name": "Seaside flat",
        "canonical_name": "string",
        "area": "string",
        "quantity": 1,
        "evidence": {
          "from": [
            {
              "frame_id": null,
              "timestamp_ms": null,
              "url": null
            }
          ],
          "to": [
            {
              "frame_id": null,
              "timestamp_ms": null,
              "url": null
            }
          ]
        },
        "note": "string"
      }
    ],
    "not_seen": [
      {
        "name": "Seaside flat",
        "canonical_name": "string",
        "area": "string",
        "quantity": 1,
        "evidence": {
          "from": [
            {
              "frame_id": null,
              "timestamp_ms": null,
              "url": null
            }
          ],
          "to": [
            {
              "frame_id": null,
              "timestamp_ms": null,
              "url": null
            }
          ]
        }
      }
    ],
    "moved": [
      {
        "name": "Seaside flat",
        "canonical_name": "string",
        "area": "string",
        "quantity": 1,
        "evidence": {
          "from": [
            {
              "frame_id": null,
              "timestamp_ms": null,
              "url": null
            }
          ],
          "to": [
            {
              "frame_id": null,
              "timestamp_ms": null,
              "url": null
            }
          ]
        },
        "from_area": "string"
      }
    ],
    "quantity_changed": [
      {
        "name": "Seaside flat",
        "canonical_name": "string",
        "area": "string",
        "quantity": 1,
        "evidence": {
          "from": [
            {
              "frame_id": null,
              "timestamp_ms": null,
              "url": null
            }
          ],
          "to": [
            {
              "frame_id": null,
              "timestamp_ms": null,
              "url": null
            }
          ]
        },
        "from_quantity": 1
      }
    ],
    "condition_changed": [
      {
        "name": "Seaside flat",
        "canonical_name": "string",
        "area": "string",
        "quantity": 1,
        "evidence": {
          "from": [
            {
              "frame_id": null,
              "timestamp_ms": null,
              "url": null
            }
          ],
          "to": [
            {
              "frame_id": null,
              "timestamp_ms": null,
              "url": null
            }
          ]
        },
        "from_condition": "new",
        "to_condition": "new"
      }
    ]
  },
  "issues": {
    "new": [
      {
        "title": "…",
        "area": "string",
        "severity": "low",
        "evidence": {
          "from": [
            {
              "frame_id": null,
              "timestamp_ms": null,
              "url": null
            }
          ],
          "to": [
            {
              "frame_id": null,
              "timestamp_ms": null,
              "url": null
            }
          ]
        }
      }
    ],
    "resolved": [
      {
        "title": "…",
        "area": "string",
        "severity": "low",
        "evidence": {
          "from": [
            {
              "frame_id": null,
              "timestamp_ms": null,
              "url": null
            }
          ],
          "to": [
            {
              "frame_id": null,
              "timestamp_ms": null,
              "url": null
            }
          ]
        },
        "note": "string"
      }
    ],
    "not_seen": [
      {
        "title": "…",
        "area": "string",
        "severity": "low",
        "evidence": {
          "from": [
            {
              "frame_id": null,
              "timestamp_ms": null,
              "url": null
            }
          ],
          "to": [
            {
              "frame_id": null,
              "timestamp_ms": null,
              "url": null
            }
          ]
        }
      }
    ]
  }
}
```

## Scans

Declare a scan, upload its video or frames, start it, follow it live, cancel it.

### GET /scans/{id}

**Status of a scan**

Poll until `status` is `complete` or `failed`. A failed scan costs nothing: it is charged at start and refunded on failure, and `charge.refunded` says so. Needs `scans:read`.

- Auth: API key (`Authorization: Bearer sc_…`)
- Operation id: `getScan`

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string (pattern ^scn_[0-9a-f]{32}$) | yes |  |

**Responses**

- `200` — Scan (Scan)
- `401` — Error response (Error)
- `403` — Error response (Error)
- `404` — Error response (Error)
- `429` — Too many requests. Retry after the number of seconds in `Retry-After`. (Error)

Example `200` response:

```json
{
  "object": "scan",
  "id": "spc_3f9a…",
  "space_id": "spc_3f9a…",
  "livemode": true,
  "status": "awaiting_upload",
  "stage": "ingesting",
  "input": {
    "kind": "frames",
    "language": "en",
    "frame_count": 1,
    "video": {
      "content_type": "string",
      "content_length": 1
    },
    "has_transcript": true
  },
  "charge": {
    "kind": "free_scan",
    "price_cents": 100,
    "refunded": false
  },
  "error": {
    "code": "string",
    "message": "string"
  },
  "upload": {
    "kind": "frames",
    "method": "PUT",
    "url": "https://…",
    "url_template": "string",
    "headers": {},
    "frame_count": 1,
    "max_bytes": 1,
    "content_types": [
      "string"
    ],
    "expires_at": "2026-10-05T13:57:02.000Z"
  },
  "created_at": "2026-10-05T13:54:37.000Z",
  "started_at": "2026-10-05T13:54:40.000Z",
  "completed_at": "2026-10-05T13:57:02.000Z"
}
```

### DELETE /scans/{id}

**Cancel a scan that has not started**

Nothing was charged; its uploads are deleted. Needs `scans:write`.

- Auth: API key (`Authorization: Bearer sc_…`)
- Operation id: `cancelScan`

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string (pattern ^scn_[0-9a-f]{32}$) | yes |  |

**Responses**

- `204` — Cancelled
- `401` — Error response (Error)
- `403` — Error response (Error)
- `404` — Error response (Error)
- `409` — Error response (Error)
- `429` — Too many requests. Retry after the number of seconds in `Retry-After`. (Error)

### GET /scans/{id}/events

**Follow a scan in real time**

A `text/event-stream` (Server-Sent Events) that sends the scan (the same object as `GET /scans/{id}`, without `upload`) as an event named `scan` at once, then every time its status, stage or error changes, and closes after the final one (`complete`, `failed` or `cancelled`). A `ping` event every 15 seconds keeps the connection alive. One stream lasts at most 30 minutes; reconnect to keep following. An account holds at most 10 streams open at once (by default): one more answers 429 with `Retry-After`; close one, or poll `GET /scans/{id}`. Needs `scans:read`.

- Auth: API key (`Authorization: Bearer sc_…`)
- Operation id: `streamScanEvents`

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string (pattern ^scn_[0-9a-f]{32}$) | yes |  |

**Responses**

- `200` — Event stream: `event: scan` with the scan as JSON data; `event: ping` with no data. (text/event-stream)
- `401` — Error response (Error)
- `403` — Error response (Error)
- `404` — Error response (Error)
- `429` — Too many requests. Retry after the number of seconds in `Retry-After`. (Error)

### PUT /scans/{id}/frames/{index}

**Upload one frame of a frames scan**

The raw image as the body: JPEG, PNG or WebP (recognised by its bytes), at most 2 MB. Sending a frame again replaces it. A test scan checks the frame the same way and only records that it arrived: nothing of a test scan is stored (its result is the demo). Needs `scans:write`.

- Auth: API key (`Authorization: Bearer sc_…`)
- Operation id: `uploadScanFrame`

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string (pattern ^scn_[0-9a-f]{32}$) | yes |  |
| `index` | path | integer (≥ 0, ≤ 119) | yes |  |

**Request body** (`image/jpeg, image/png, image/webp`, required): binary

**Responses**

- `200` — Frame stored (ScanUpload)
- `400` — Error response (Error)
- `401` — Error response (Error)
- `403` — Error response (Error)
- `404` — Error response (Error)
- `409` — Error response (Error)
- `413` — Error response (Error)
- `415` — Error response (Error)
- `429` — Too many requests. Retry after the number of seconds in `Retry-After`. (Error)

Example `200` response:

```json
{
  "object": "scan_frame",
  "scan_id": "scn_8a21…",
  "index": 1,
  "bytes": 1,
  "content_type": "string"
}
```

### PUT /scans/{id}/video

**Upload the video of a video scan through the API**

Only when `upload.url` points here (no direct upload link available; always for a test scan, whose video is counted and dropped, never stored). `Content-Length` and `Content-Type` must match what was declared, and the body must be that size. At most 99614720 bytes (95 MiB): a bigger video answers 413 `FILE_TOO_LARGE` and goes to the direct upload link instead. Needs `scans:write`.

- Auth: API key (`Authorization: Bearer sc_…`)
- Operation id: `uploadScanVideo`

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string (pattern ^scn_[0-9a-f]{32}$) | yes |  |

**Request body** (`video/mp4, video/quicktime, video/x-m4v, video/3gpp, video/webm, video/x-matroska, video/matroska`, required): binary

**Responses**

- `200` — Video stored (ScanUpload)
- `400` — Error response (Error)
- `401` — Error response (Error)
- `403` — Error response (Error)
- `404` — Error response (Error)
- `409` — Error response (Error)
- `413` — Error response (Error)
- `415` — Error response (Error)
- `429` — Too many requests. Retry after the number of seconds in `Retry-After`. (Error)

Example `200` response:

```json
{
  "object": "scan_frame",
  "scan_id": "scn_8a21…",
  "index": 1,
  "bytes": 1,
  "content_type": "string"
}
```

### POST /scans/{id}/start

**Charge the scan and start the analysis**

Checks that every frame (or the video) is uploaded, uses the free scan or charges the wallet, and starts the analysis. Answers 202 when it started the scan and 200 when the scan had already started. A failed analysis is refunded. Errors: 402 `PAYMENT_METHOD_REQUIRED` (a live scan needs a card on file: added in the console, nothing charged), `INSUFFICIENT_FUNDS` or `SPENDING_CAP_REACHED`, 409 `UPLOAD_INCOMPLETE`, 429 `SCANS_IN_PROGRESS`, 429 `RATE_LIMIT_EXCEEDED` with `Retry-After` when 5 live scans of the account failed that day because of their footage (no usable frame, an unreadable or too long video, nothing recognisable: each refunded) until midnight UTC, 503 `PAYMENTS_UNAVAILABLE` (a live scan where payments are not configured). Test keys are never charged and get the demo result. Needs `scans:write`.

- Auth: API key (`Authorization: Bearer sc_…`)
- Operation id: `startScan`

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string (pattern ^scn_[0-9a-f]{32}$) | yes |  |

**Responses**

- `200` — Already started (Scan)
- `202` — Started (Scan)
- `401` — Error response (Error)
- `402` — Error response (Error)
- `403` — Error response (Error)
- `404` — Error response (Error)
- `409` — Error response (Error)
- `429` — Too many requests. Retry after the number of seconds in `Retry-After`. (Error)
- `503` — Error response (Error)

Example `200` response:

```json
{
  "object": "scan",
  "id": "spc_3f9a…",
  "space_id": "spc_3f9a…",
  "livemode": true,
  "status": "awaiting_upload",
  "stage": "ingesting",
  "input": {
    "kind": "frames",
    "language": "en",
    "frame_count": 1,
    "video": {
      "content_type": "string",
      "content_length": 1
    },
    "has_transcript": true
  },
  "charge": {
    "kind": "free_scan",
    "price_cents": 100,
    "refunded": false
  },
  "error": {
    "code": "string",
    "message": "string"
  },
  "upload": {
    "kind": "frames",
    "method": "PUT",
    "url": "https://…",
    "url_template": "string",
    "headers": {},
    "frame_count": 1,
    "max_bytes": 1,
    "content_types": [
      "string"
    ],
    "expires_at": "2026-10-05T13:57:02.000Z"
  },
  "created_at": "2026-10-05T13:54:37.000Z",
  "started_at": "2026-10-05T13:54:40.000Z",
  "completed_at": "2026-10-05T13:57:02.000Z"
}
```

## Frames

Check one frame while a space is being filmed: usable or not, which key locations of its type it shows, and the area.

### POST /frames/check

**Check one frame while filming**

For apps that capture a space and guide the person filming: is this frame usable, which key locations does it show and, with `space_type`, which area. With `space_type` the key locations are those of that type of space (`GET /capture-profiles`: a car is checked for its VIN plate and dashboard, a boat for its seacocks), and `area` says where the frame was taken; without it, a home's: electrical_panel, water_shutoff, water_heater, smoke_detector, fire_extinguisher, router, heating_control, gas_meter, and no area. A key location is `seen` from a probability of 0.7. The raw image as the body: JPEG, PNG or WebP, at most 2 MB. About half a second; nothing is stored. Free within a daily allowance per account (429 when used up). Test keys get a fixed answer. Needs `scans:write`; a live check needs a card on file, like a live scan. Errors: 400 `VALIDATION_ERROR` (unknown `space_type`), 402 `PAYMENT_METHOD_REQUIRED` (live, no card on file: added in the console, nothing charged), 415 `INVALID_CONTENT_TYPE`, 503 `FRAME_CHECK_UNAVAILABLE`, 503 `PAYMENTS_UNAVAILABLE` (live, where payments are not configured).

- Auth: API key (`Authorization: Bearer sc_…`)
- Operation id: `checkFrame`

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `space_type` | query | string: "apartment" \| "house" \| "office" \| "warehouse" \| "vehicle" \| "boat" \| "camper" \| "shop" \| "garage" \| "appliance" \| "other" | no | The type of space being filmed: its key locations, and the area question. |

**Request body** (`image/jpeg, image/png, image/webp`, required): binary

**Responses**

- `200` — What the frame shows (FrameCheck)
- `400` — Error response (Error)
- `401` — Error response (Error)
- `402` — Error response (Error)
- `403` — Error response (Error)
- `413` — Error response (Error)
- `415` — Error response (Error)
- `429` — Too many requests. Retry after the number of seconds in `Retry-After`. (Error)
- `503` — Error response (Error)

Example `200` response:

```json
{
  "object": "frame_check",
  "livemode": true,
  "space_type": "apartment",
  "usable": true,
  "quality": {
    "score": 0,
    "level": "unusable"
  },
  "shows_space": 0,
  "area": {
    "value": "entrance",
    "probability": 0
  },
  "key_locations": [
    {
      "kind": "electrical_panel",
      "probability": 0,
      "seen": true
    }
  ]
}
```

## Capture profiles

What to film for each type of space: its key locations, each with a hint, and the areas the frame check tells apart. Public, no key.

### GET /capture-profiles

**What to film, for every type of space**

For each type of space, its key locations (what people and agents ask about first: the fuse box of a flat, the VIN plate of a car, the seacocks of a boat), each with a one-line hint of what to film and whether it is usually there (`expected`), and its areas. The same for everyone: no key, cacheable. Use it to show a checklist while filming, with `POST /frames/check?space_type=`.

- Auth: public (no credentials)
- Operation id: `listCaptureProfiles`

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `language` | query | string: "en" \| "it" \| "es" \| "fr" \| "de" \| "pt" \| "nl" | no | The language of the labels and hints: `en`, `it`, `es`, `fr`, `de`, `pt`, `nl` (default `en`). |

**Responses**

- `200` — Every capture profile (CaptureProfileList)
- `400` — Error response (Error)

Example `200` response:

```json
{
  "object": "list",
  "data": [
    {
      "object": "capture_profile",
      "space_type": "apartment",
      "language": "en",
      "key_locations": [
        {
          "kind": "electrical_panel",
          "label": "…",
          "hint": "string",
          "expected": true
        }
      ],
      "areas": [
        {
          "value": "entrance",
          "label": "…"
        }
      ]
    }
  ]
}
```

### GET /capture-profiles/{space_type}

**What to film, for one type of space**

One type of space: its key locations with a hint each, and its areas. Errors: 400 `VALIDATION_ERROR` (unknown language), 404 `NOT_FOUND` (unknown type).

- Auth: public (no credentials)
- Operation id: `getCaptureProfile`

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `space_type` | path | string: "apartment" \| "house" \| "office" \| "warehouse" \| "vehicle" \| "boat" \| "camper" \| "shop" \| "garage" \| "appliance" \| "other" | yes |  |
| `language` | query | string: "en" \| "it" \| "es" \| "fr" \| "de" \| "pt" \| "nl" | no | The language of the labels and hints: `en`, `it`, `es`, `fr`, `de`, `pt`, `nl` (default `en`). |

**Responses**

- `200` — The capture profile (CaptureProfile)
- `400` — Error response (Error)
- `404` — Error response (Error)

Example `200` response:

```json
{
  "object": "capture_profile",
  "space_type": "apartment",
  "language": "en",
  "key_locations": [
    {
      "kind": "electrical_panel",
      "label": "…",
      "hint": "string",
      "expected": true
    }
  ],
  "areas": [
    {
      "value": "entrance",
      "label": "…"
    }
  ]
}
```

## Search

Search every space the key can read, in any language.

### GET /search

**Search across all your spaces**

Finds the records (items, areas, issues, key locations, instructions, facts, questions and answers) that best match a question in natural language, across every space this key can read, in any of 100+ languages: "where did I see a ladder?". Each result names its space, scan and record, so it can be opened with the other operations. The index holds only screened, redacted content: no result ever contains a sensitive value, whatever the key's scopes. A space appears once its current scan has been indexed: after a scan completes, or after the deep pass of a space scanned with the app. Test keys search the demo space, without a model. Limits: 20 a minute and, by default, 1000 a day per account.

- Auth: API key (`Authorization: Bearer sc_…`)
- Operation id: `searchSpaces`

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `q` | query | string (min 1, max 300) | yes |  |
| `space_id` | query | string (pattern ^(app\|spc)_) | no | Searches one space only. |
| `limit` | query | integer (≥ 1, ≤ 25) | no |  |

**Responses**

- `200` — Best matches first (SearchResults)
- `400` — Error response (Error)
- `401` — Error response (Error)
- `403` — Error response (Error)
- `404` — Error response (Error)
- `429` — Too many requests. Retry after the number of seconds in `Retry-After`. (Error)
- `503` — Error response (Error)

Example `200` response:

```json
{
  "object": "search_results",
  "query": "string",
  "data": [
    {
      "space_id": "spc_3f9a…",
      "space_name": "string",
      "scan_id": "scn_8a21…",
      "record_type": "area",
      "record_id": "rec_…",
      "title": "…",
      "area": "string",
      "text": "…",
      "score": 1
    }
  ]
}
```

## Media

Evidence frames, served through signed links.

### GET /media/{key}

**Serve a signed v1 evidence frame**

- Auth: public (no credentials)
- Operation id: `getEvidenceFrame`

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `key` | path | string (pattern ^(frames\|api)/) | yes | Storage key of the frame, exactly as in the signed URL: `frames/…` for spaces scanned with the app, `api/…` for scans made through the API. |
| `token` | query | string | yes |  |
| `expires` | query | integer | yes |  |

**Responses**

- `200` — Original evidence frame bytes (image/jpeg)
- `401` — Error response (Error)
- `404` — Error response (Error)
- `429` — Too many requests. Retry after the number of seconds in `Retry-After`. (Error)

## Wallet and billing

Balance, top-ups through Stripe, automatic top-up, ledger and invoices.

### GET /wallet

**What this key has to spend**

For agents: the balance, the free scans left, the price of a scan and whether a card is on file (`card_on_file`: live scans need one). A test key reads a fixed test-mode wallet instead (balance 0, no free scan, `card_on_file` false, automatic recharge off, the live price): test scans cost nothing and need no card. Needs the `scans:read` scope.

- Auth: API key (`Authorization: Bearer sc_…`)
- Operation id: `getWallet`

**Responses**

- `200` — Wallet (Wallet)
- `401` — Error response (Error)
- `403` — Error response (Error)
- `429` — Too many requests. Retry after the number of seconds in `Retry-After`. (Error)

Example `200` response:

```json
{
  "object": "wallet",
  "currency": "usd",
  "balance_cents": 100,
  "free_scans_remaining": 0,
  "scan_price_cents": 100,
  "card_on_file": true,
  "tax_added": true,
  "auto_recharge": {
    "enabled": true,
    "threshold_cents": 100,
    "amount_cents": 100,
    "payment_method_saved": true,
    "last_error": "string"
  },
  "terms_accepted": {
    "version": "string",
    "accepted_at": "2026-10-05T13:57:02.000Z"
  }
}
```

## Account and console

Sign-in, the key in use, the console overview and the audit log.

### GET /me

**Get the current API key user**

- Auth: API key (`Authorization: Bearer sc_…`)
- Operation id: `getCurrentKey`

**Responses**

- `200` — Current API key user (MeResponse)
- `401` — Error response (Error)
- `429` — Too many requests. Retry after the number of seconds in `Retry-After`. (Error)

Example `200` response:

```json
{
  "user": {
    "id": "spc_3f9a…",
    "email": "you@example.com",
    "is_pro": true
  },
  "key": {
    "id": "spc_3f9a…",
    "mode": "live",
    "scopes": [
      "spaces:read"
    ]
  }
}
```

### POST /auth/device

**Start a device sign-in (the CLI)**

A terminal that has no browser session asks for a code. It shows `user_code` and `verification_url` to the person, who opens the console and types the code (never a link with the code in it: a link could be sent by someone else to get their terminal approved), then polls `POST /auth/device/token` with `device_code` every `interval` seconds. The code lives `expires_in` seconds. Public; limited per IP.

- Auth: public (no credentials)
- Operation id: `startDeviceSignIn`

**Request body** (`application/json`): DeviceStartRequest

```json
{
  "name": "Seaside flat",
  "mode": "live"
}
```

**Responses**

- `201` — Codes for the terminal and the person (DeviceStart)
- `400` — Error response (Error)
- `429` — Too many requests. Retry after the number of seconds in `Retry-After`. (Error)

Example `201` response:

```json
{
  "device_code": "string",
  "user_code": "BCDF-2345",
  "verification_url": "string",
  "expires_in": 1,
  "interval": 1
}
```

### POST /auth/device/token

**Poll a device sign-in**

Answers 400 `AUTHORIZATION_PENDING` until the person decides, 400 `SLOW_DOWN` when polled faster than `interval`, 400 `DEVICE_CODE_EXPIRED` after ten minutes, 403 `ACCESS_DENIED` when denied, and the new API key exactly once when approved. Public.

- Auth: public (no credentials)
- Operation id: `claimDeviceSignIn`

**Request body** (`application/json`, required): DeviceTokenRequest

```json
{
  "device_code": "string"
}
```

**Responses**

- `200` — The key, once (DeviceToken)
- `400` — Error response (Error)
- `403` — Error response (Error)
- `429` — Too many requests. Retry after the number of seconds in `Retry-After`. (Error)

Example `200` response:

```json
{
  "api_key": "string",
  "key": {
    "id": "spc_3f9a…",
    "name": "Seaside flat",
    "mode": "live",
    "prefix": "string",
    "scopes": [
      "string"
    ],
    "monthly_cap_cents": 100,
    "created_at": "2026-10-05T13:54:37.000Z"
  },
  "user": {
    "id": "spc_3f9a…",
    "email": "you@example.com",
    "name": "Seaside flat",
    "is_pro": true,
    "is_admin": true
  }
}
```

## Service

The API itself.

### GET /

**API metadata**

Where the contract, the docs and the MCP server are, and the input limits of this deployment (`limits`).

- Auth: public (no credentials)
- Operation id: `getApiInfo`

**Responses**

- `200` — API metadata (ApiInfo)

Example `200` response:

```json
{
  "name": "space-context-api",
  "version": "v1",
  "openapi": "string",
  "docs": "string",
  "llms": "string",
  "llms_full": "string",
  "mcp": "string",
  "console": "string",
  "limits": {
    "max_video_bytes": 1,
    "max_video_seconds": 1,
    "max_photos": 1
  }
}
```

### GET /openapi.json

**OpenAPI document**

- Auth: public (no credentials)
- Operation id: `getOpenApiDocument`

**Responses**

- `200` — OpenAPI document

## Other

### POST /agent/credits

**Pay with x402 and get an API key (agents)**

For an agent with a wallet and no account: pay in USDC on **Base** or **Solana** with [x402](https://x402.org) v2 and receive an API key. No key, no sign-in: the payment is the credential, and the paying address is the account. Any x402 v2 client works (for example `@x402/fetch` with `@x402/evm` and `@x402/svm`): the network fee is paid by the facilitator, the payer needs only USDC.

1. Call without `PAYMENT-SIGNATURE`: 402 `PAYMENT_REQUIRED` with the `PAYMENT-REQUIRED` header (base64 JSON, also in the body as `payment_required`). `accepts` has one entry per network, Base first: scheme `exact`, the network (CAIP-2), the amount in atomic USDC units (6 decimals: US$1.00 = `1000000`), the USDC contract or mint, the address to pay, and `extra` (the EIP-712 domain of USDC on Base; on Solana `feePayer`, the facilitator account that pays the network fee). `extensions.bazaar` describes the call for x402 catalogs.
2. Pick one entry, sign it, and send **the same request, with the same body**, adding `PAYMENT-SIGNATURE` (base64 JSON PaymentPayload with that entry, unchanged, as `accepted`). On Base the payload is an EIP-3009 `transferWithAuthorization`; on Solana a transaction partially signed by the payer: SetComputeUnitLimit, SetComputeUnitPrice, a `TransferChecked` of exactly the amount into the USDC account of `payTo`, optional memo, fee payer `extra.feePayer`. The payment is verified and settled by the facilitator; the answer carries `PAYMENT-RESPONSE` with the transaction.

On mainnet (Base `eip155:8453`, Solana `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp`) the payment is credited to the account's wallet (`amount_cents`, then US$1.00 per live scan; reading, search and answers are included; agents have no free scan; the credit does not expire) and the key is **live**. On testnets (Base Sepolia `eip155:84532`, Solana devnet `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1`) USDC is free, so the payment credits nothing: the key is a **test** key (demo space, no charges, no model). The same paying address always gets the same account; a Base wallet and a Solana wallet are two accounts.

**Store `api_key` from your first purchase: it is shown once.** A signed authorization can be submitted by whoever holds it, so a payment alone returns a key only when it opens the account. Later payments from the same wallet top up the same account and return only `api_key_prefix`. An existing key is replaced only with that key: `rotate_key: true` plus `Authorization: Bearer <the current key>` (the previous key is revoked). The first key of the other mode (e.g. a live key for an account that so far paid on testnet) needs `Authorization: Bearer <one of the account's current keys>`. Both are checked before any payment is taken (403 `AGENT_KEY_REQUIRED`, nothing charged). A payment replayed with the same transaction is recognised: nothing is credited or minted twice.

400 `PAYMENT_INVALID` when the signature header is malformed or does not match the requirements (amount, network, asset, address, fee payer, the payer's signature), before anything is sent to the facilitator. 402 `PAYMENT_FAILED` when the facilitator rejects or cannot settle the payment (`reason`; nothing was charged). 503 `PAYMENTS_UNAVAILABLE` when the chosen network cannot be served right now (nothing was charged). 404 when agent payments are off on this deployment. Limited per IP.

- Auth: public (no credentials)
- Operation id: `buyAgentCredits`

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `PAYMENT-SIGNATURE` | header | string (max 16384) | no | base64 of the JSON x402 v2 PaymentPayload: `x402Version: 2`, `accepted` (one entry of `accepts`, unchanged), and `payload`: on Base `{ signature, authorization: { from, to, value, validAfter, validBefore, nonce } }`, on Solana `{ transaction }` (base64 of the partially signed transaction). |

**Request body** (`application/json`): AgentCreditRequest

```json
{
  "amount_cents": 100,
  "rotate_key": false
}
```

**Responses**

- `200` — Paid: the account, its credit (mainnet) and its key (AgentCredit)
- `400` — Invalid body, or `PAYMENT_INVALID` (AgentPaymentError)
- `402` — `PAYMENT_REQUIRED` (pay first) or `PAYMENT_FAILED` (rejected or not settled; nothing was charged) (AgentPaymentError)
- `403` — `AGENT_KEY_REQUIRED`: changing the keys of an existing account needs one of its current keys (nothing was charged) (AgentPaymentError)
- `404` — Error response (Error)
- `429` — Too many requests. Retry after the number of seconds in `Retry-After`. (Error)
- `500` — Settled but not recorded: reconciled by hand (`transaction`) (AgentPaymentError)
- `502` — The settlement could not be confirmed (`nonce` on Base, `signature` on Solana) (AgentPaymentError)
- `503` — Error response (Error)

Example `200` response:

```json
{
  "object": "agent_credit",
  "network": "eip155:8453",
  "payer": "string",
  "transaction": "string",
  "livemode": true,
  "amount_cents": 100,
  "balance_cents": 100,
  "api_key": "string",
  "api_key_prefix": "string",
  "notice": "string",
  "docs": "string"
}
```

## Objects

The fields of each object of the contract. Required fields are marked.

### ApiInfo

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | "space-context-api" | yes |  |
| `version` | "v1" | yes |  |
| `openapi` | string | yes |  |
| `docs` | string (uri) | no |  |
| `llms` | string (uri) | no |  |
| `llms_full` | string (uri) | no |  |
| `mcp` | string (uri) | no |  |
| `console` | string (uri) | no |  |
| `limits` | object | yes | What one scan may send to this deployment. |

### ConsoleUser

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `email` | string (email) | yes |  |
| `name` | string \| null | yes |  |
| `is_pro` | boolean | yes |  |
| `is_admin` | boolean | yes | True only for Space Context staff accounts, which the console shows extra internal pages; false for everyone else. |

### MeResponse

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `user` | object | yes |  |
| `key` | object | yes |  |

### SpacePlacement

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `anchor` | string \| null | yes | A fixed landmark the thing is placed against: a door, a window, the hob, a radiator. Never the thing itself. |
| `anchor_item_id` | string \| null | yes | The id of the item in this document that is the anchor, when the anchor is one of its items. |
| `relation` | PlacementRelation | yes | How a thing sits against its anchor. `left_of`, `right_of`, `in_front_of`, `behind` and `opposite` hold only from the placement's `viewpoint`; the others hold from anywhere. `unknown`: the recording does not show where it is. |
| `viewpoint` | string \| null | yes | Where a person stands and what they face for the relation to hold ("inside the flat, facing the entrance door"). Always set for a lateral relation. One canonical viewpoint per area: facing the entrance door from inside for an entrance or hallway, standing in the doorway one enters it from and looking in for any other area. An agent describing the place to someone mirrors left and right only when it knows that person's own viewpoint; otherwise it repeats this one. |
| `detail` | string \| null | yes | Height, the part of the anchor, or a second anchor: "at chest height, above the shoe rack". |

### SpaceEvidenceRef

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `frame_id` | string | yes |  |
| `timestamp_ms` | integer \| null | yes |  |

### SpaceVerification

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `status` | string: "verified" \| "unconfirmed" \| "not_visual" | yes |  |
| `probability` | number \| null (≥ 0, ≤ 1) | yes | Best probability over the frames checked. Null for `not_visual`. |
| `frame_id` | string \| null | yes | The frame that shows it best; it is also the first of the record's `evidence`. |

### SpaceFrame

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `index` | integer | yes |  |
| `timestamp_ms` | integer \| null | yes |  |
| `url` | string \| null (uri) | yes |  |
| `url_expires_at` | string \| null (date-time) | yes |  |

### SpaceCondition

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `code` | ConditionCode | yes |  |
| `label` | string \| null | yes | The label as written in the language of the report ("Good", "Buono", "Bueno", "Gut"...); `code` is the same in every language. |
| `notes` | string \| null | yes |  |

### SpaceArea

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `name` | string | yes |  |
| `type` | AreaType \| null | yes |  |
| `level` | string \| null | yes |  |
| `condition` | SpaceCondition | yes |  |
| `description` | string \| null | yes |  |
| `layout` | string \| null | yes |  |
| `orientation` | string \| null | yes | One sentence that places the main fixed things of the area from its canonical viewpoint (facing the entrance door from inside for an entrance or hallway; standing in the doorway one enters it from, looking in, for any other area): "Coming in from the hallway: window ahead, bed on the left, desk on the right." Null when the recording does not show enough, and always null at the base level. |
| `features` | array of string | yes |  |
| `access_from` | array of string | yes |  |
| `item_ids` | array of string | yes |  |
| `issue_ids` | array of string | yes |  |
| `evidence` | array of SpaceEvidenceRef | yes |  |
| `confidence` | number \| null (≥ 0, ≤ 1) | yes |  |

### SpaceItem

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `area_id` | string | yes |  |
| `name` | string | yes |  |
| `canonical_name` | string \| null | yes |  |
| `description` | string \| null | yes | What it is, where it is with the viewpoint, what was said about it (use, settings, quirks, what to watch out for), its condition when notable, in one paragraph of 1-3 sentences in the report language. Never a sensitive value. Null at the base level and on app spaces. |
| `category` | ItemCategory \| null | yes |  |
| `subcategory` | string \| null | yes |  |
| `quantity` | integer \| null (≥ 1) | yes |  |
| `location` | string \| null | yes | Where it is inside the area, as one sentence that carries the viewpoint when left and right matter: "on the wall left of the entrance door, facing the door from inside; on your right as you enter". |
| `placement` | SpacePlacement \| null | yes | The same place as anchor + relation + viewpoint. Null at the base level and on app spaces. |
| `condition` | SpaceCondition \| null | yes |  |
| `brand` | string \| null | yes |  |
| `model` | string \| null | yes |  |
| `attributes` | array of string | yes |  |
| `visible_text` | array of string | yes |  |
| `aliases` | array of string | yes |  |
| `note` | string \| null | yes |  |
| `issue_ids` | array of string | yes |  |
| `previous_item_id` | string \| null | yes | The same object in the previous scan (`continuity.previous_scan_id`), when this scan was checked against it. |
| `evidence` | array of SpaceEvidenceRef | yes |  |
| `verification` | SpaceVerification \| null | yes | Checked again on its frames. Null at the base level, on documents made before verification existed, and where it could not be computed. |
| `confidence` | number \| null (≥ 0, ≤ 1) | yes |  |

### SpaceIssue

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `area_id` | string \| null | yes |  |
| `item_id` | string \| null | yes |  |
| `title` | string | yes |  |
| `description` | string \| null | yes |  |
| `category` | IssueCategory \| null | yes |  |
| `severity` | IssueSeverity \| null | yes |  |
| `suggested_action` | string \| null | yes |  |
| `previous_issue_id` | string \| null | yes | The same issue in the previous scan, still there. |
| `evidence` | array of SpaceEvidenceRef | yes |  |
| `verification` | SpaceVerification \| null | yes | Checked again on its frames. Null at the base level, on documents made before verification existed, and where it could not be computed. |
| `confidence` | number \| null (≥ 0, ≤ 1) | yes |  |

### SpaceKeyLocation

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `kind` | KeyLocationKind | yes |  |
| `label` | string | yes |  |
| `description` | string \| null | yes | What it is, where it is with the viewpoint, what was said about it (how to reach or operate it, what to watch out for), its condition when notable, in one paragraph. Never a code or a password. Null at the base level and on app spaces. |
| `area_id` | string \| null | yes |  |
| `location` | string \| null | yes | Specific enough to walk straight to it, with the viewpoint when left and right matter. |
| `placement` | SpacePlacement \| null | yes | The same place as anchor + relation + viewpoint. Null at the base level and on app spaces. |
| `notes` | string \| null | yes |  |
| `evidence` | array of SpaceEvidenceRef | yes |  |
| `verification` | SpaceVerification \| null | yes | Checked again on its frames. Null at the base level, on documents made before verification existed, and where it could not be computed. |
| `confidence` | number \| null (≥ 0, ≤ 1) | yes |  |

### SpaceInstruction

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `title` | string | yes |  |
| `area_id` | string \| null | yes |  |
| `item_id` | string \| null | yes |  |
| `steps` | array of string | yes |  |
| `source_quote` | string \| null | yes |  |
| `transcript_segment_ids` | array of string | yes |  |
| `confidence` | number \| null (≥ 0, ≤ 1) | yes |  |

### SpaceFact

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `kind` | FactKind | yes |  |
| `statement` | string | yes |  |
| `value` | string \| null | yes |  |
| `area_id` | string \| null | yes |  |
| `sensitive` | boolean | yes |  |
| `redacted` | boolean | yes |  |
| `transcript_segment_ids` | array of string | yes |  |
| `evidence` | array of SpaceEvidenceRef | yes |  |
| `confidence` | number \| null (≥ 0, ≤ 1) | yes |  |

### SpaceQA

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `question` | string | yes |  |
| `answer` | string | yes |  |
| `source` | string: "owner" \| "generated" | yes |  |
| `category` | string \| null | yes |  |
| `area_id` | string \| null | yes |  |

### SpaceTranscriptSegment

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `start_ms` | integer | yes |  |
| `end_ms` | integer | yes |  |
| `text` | string | yes |  |

### SpaceContinuity

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `previous_scan_id` | string (pattern ^scn_) | yes |  |
| `absent_items` | array of object | yes |  |
| `resolved_issues` | array of object | yes |  |

### SpaceDocument

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `object` | string: "space_document" | yes |  |
| `schema_version` | string: "1.0" | yes |  |
| `space` | object | yes |  |
| `scan` | object | yes |  |
| `enrichment` | object | yes |  |
| `summary` | string \| null | yes |  |
| `confidence` | number \| null (≥ 0, ≤ 1) | yes |  |
| `stats` | object | yes |  |
| `features` | array of string | yes |  |
| `areas` | array of SpaceArea | yes |  |
| `items` | array of SpaceItem | yes |  |
| `issues` | array of SpaceIssue | yes |  |
| `key_locations` | array of SpaceKeyLocation | yes |  |
| `instructions` | array of SpaceInstruction | yes |  |
| `facts` | array of SpaceFact | yes |  |
| `qa` | array of SpaceQA | yes |  |
| `transcript` | object | no |  |
| `frames` | array of SpaceFrame | yes |  |
| `continuity` | SpaceContinuity \| null | yes |  |
| `coverage` | object | yes |  |

### SpaceSummary

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `object` | string: "space" | yes |  |
| `id` | string (pattern ^(app\|spc)_) | yes |  |
| `source` | string: "app" \| "api" | yes |  |
| `name` | string | yes |  |
| `space_type` | SpaceType | yes |  |
| `created_at` | string (date-time) | yes |  |
| `updated_at` | string (date-time) | yes |  |
| `latest_scan` | object \| null | yes |  |

### SpaceListResponse

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | array of SpaceSummary | yes |  |
| `next_cursor` | string \| null | yes |  |

### ScanSummary

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `object` | string: "scan" | yes |  |
| `id` | string (pattern ^scn_) | yes |  |
| `space_id` | string (pattern ^(app\|spc)_) | yes |  |
| `analyzed_at` | string \| null (date-time) | yes |  |
| `areas` | integer | yes |  |
| `items` | integer | yes |  |
| `issues` | integer | yes |  |
| `current` | boolean | yes |  |

### ScanListResponse

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | array of ScanSummary | yes |  |

### SearchResults

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `object` | "search_results" | yes |  |
| `query` | string | yes |  |
| `data` | array of object | yes |  |

### SpaceAnswer

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `object` | "answer" | yes |  |
| `space_id` | string | yes |  |
| `scan_id` | string | yes |  |
| `question` | string | yes |  |
| `answer` | string | yes |  |
| `documented` | boolean | yes |  |
| `citations` | array of object | yes |  |

### SkillFile

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `path` | string | yes |  |
| `media_type` | string: "text/markdown" \| "application/json" | yes |  |
| `content` | string | yes |  |

### SkillBundle

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `object` | "skill" | yes |  |
| `name` | string (max 64, pattern ^[a-z0-9]+(-[a-z0-9]+)*$) | yes |  |
| `space_id` | string | yes |  |
| `scan_id` | string | yes |  |
| `files` | array of SkillFile | yes |  |

### SpaceDiff

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `object` | "space_diff" | yes |  |
| `space_id` | string | yes |  |
| `from` | object | yes |  |
| `to` | object | yes |  |
| `level` | string: "base" \| "deep" | yes |  |
| `linked` | boolean | yes | True when `to` was checked against `from` itself (consecutive scans of a space created through the API): its links and confirmed removals are used. Otherwise records are matched by content only, and nothing can be confirmed removed or resolved. |
| `summary` | object | yes |  |
| `areas` | object | yes |  |
| `items` | object | yes |  |
| `issues` | object | yes |  |

### Wallet

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `object` | "wallet" | yes |  |
| `currency` | "usd" | yes |  |
| `balance_cents` | integer | yes |  |
| `free_scans_remaining` | integer (≥ 0) | yes |  |
| `scan_price_cents` | integer (≥ 1) | yes |  |
| `card_on_file` | boolean | yes | A real card stands behind the account: a card saved in the console, a paid top-up, or a settled x402 payment on mainnet. Starting a live scan, and creating a live key that can scan, need it: without it they answer 402 `PAYMENT_METHOD_REQUIRED`. Adding a card charges nothing. |
| `tax_added` | boolean | no | Whether tax is added on top of top-ups and automatic recharges where it applies (such as VAT), from the billing address and tax ID, and shown on the payment page and the invoice. A business with a valid VAT number in another EU country is invoiced under the reverse charge. The balance always receives the amount before tax. |
| `auto_recharge` | object | yes |  |
| `terms_accepted` | object \| null | no | Console only (absent from `GET /wallet`): the first time this account accepted the current version of the billing terms, with the box of a payment or card page; null if never, or only an older version. The box is still ticked on every page (`accept_terms`), and the first acceptance is kept on later payments for a dispute. |

### SpaceCreateRequest

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string (min 1, max 120) | yes |  |
| `description` | string \| null (max 2000) | no |  |
| `space_type` | string \| null: "apartment" \| "house" \| "office" \| "warehouse" \| "vehicle" \| "boat" \| "camper" \| "shop" \| "garage" \| "appliance" \| "other" \| "null" | no |  |
| `external_id` | string \| null (max 128) | no |  |

### ApiSpace

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `object` | "space" | yes |  |
| `id` | string (pattern ^spc_[0-9a-f]{32}$) | yes |  |
| `source` | "api" | yes |  |
| `livemode` | boolean | yes |  |
| `name` | string | yes |  |
| `description` | string \| null | yes |  |
| `space_type` | string \| null | yes |  |
| `external_id` | string \| null | yes |  |
| `created_at` | string \| null (date-time) | yes |  |
| `updated_at` | string \| null (date-time) | yes |  |

### ScanCreateRequest

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `input` | object | yes |  |

### Scan

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `object` | "scan" | yes |  |
| `id` | string (pattern ^scn_[0-9a-f]{32}$) | yes |  |
| `space_id` | string | yes |  |
| `livemode` | boolean | yes |  |
| `status` | string: "awaiting_upload" \| "queued" \| "processing" \| "complete" \| "failed" \| "cancelled" | yes |  |
| `stage` | string \| null: "ingesting" \| "screening" \| "observing" \| "fusing" \| "enriching" \| "saving" \| "verifying" \| "null" | yes |  |
| `input` | object | yes |  |
| `charge` | object | yes |  |
| `error` | object \| null | yes |  |
| `upload` | object \| null | yes |  |
| `created_at` | string \| null (date-time) | yes |  |
| `started_at` | string \| null (date-time) | yes |  |
| `completed_at` | string \| null (date-time) | yes |  |

### ScanUpload

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `object` | string: "scan_frame" \| "scan_video" | yes |  |
| `scan_id` | string | yes |  |
| `index` | integer | no |  |
| `bytes` | integer | yes |  |
| `content_type` | string | no |  |

### FrameCheck

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `object` | string: "frame_check" | yes |  |
| `livemode` | boolean | yes |  |
| `space_type` | string \| null: "apartment" \| "house" \| "office" \| "warehouse" \| "vehicle" \| "boat" \| "camper" \| "shop" \| "garage" \| "appliance" \| "other" \| "null" | yes | The `space_type` asked, or null. |
| `usable` | boolean | yes | Sharp, lit and showing the space: worth keeping in a scan. |
| `quality` | object | yes |  |
| `shows_space` | number (≥ 0, ≤ 1) | yes | Probability that the frame shows the real thing being filmed (a place, a vehicle, a boat, an appliance), not a drawing, document or screen. |
| `area` | object \| null | yes | The most likely area of the space (`GET /capture-profiles`), with its probability: show it from about 0.6. Null without `space_type`, for a type with no areas (appliance), or when the frame shows none of them. |
| `key_locations` | array of object | yes | Every key location of the type of space, in its order. |

### CaptureProfile

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `object` | string: "capture_profile" | yes |  |
| `space_type` | string: "apartment" \| "house" \| "office" \| "warehouse" \| "vehicle" \| "boat" \| "camper" \| "shop" \| "garage" \| "appliance" \| "other" | yes |  |
| `language` | string: "en" \| "it" \| "es" \| "fr" \| "de" \| "pt" \| "nl" | yes |  |
| `key_locations` | array of object | yes | In the order to show them. |
| `areas` | array of object | yes | The areas `POST /frames/check?space_type=` can answer. Empty for an appliance. |

### CaptureProfileList

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `object` | string: "list" | yes |  |
| `data` | array of CaptureProfile | yes |  |

### SpaceCoverageCheck

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `kind` | KeyLocationKind | yes |  |
| `seen` | boolean | yes | At least one frame shows it (probability from 0.7). |
| `probability` | number (≥ 0, ≤ 1) | yes | The highest probability over the frames. |
| `evidence` | array of SpaceEvidenceRef | yes |  |

### DeviceStartRequest

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string (max 60) | no | What is asking, shown to the person: "CLI on MacBook". Default "CLI". |
| `mode` | string: "live" \| "test" | no | The mode of the key asked for (the person can change it). Default live. |

### DeviceStart

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `device_code` | string | yes | The terminal's secret: never shown to the person. |
| `user_code` | string | yes |  |
| `verification_url` | string (uri) | yes | The console page where the person types the code. |
| `expires_in` | integer | yes |  |
| `interval` | integer | yes | Seconds between polls. |

### DeviceTokenRequest

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `device_code` | string | yes |  |

### DeviceToken

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `api_key` | string | yes | The new key: shown once, store it. |
| `key` | object | yes |  |
| `user` | ConsoleUser | yes |  |

### AgentCreditRequest

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `amount_cents` | integer (≥ 100, ≤ 50000) | no | US cents to pay (and, on mainnet, to credit): US$1.00 is one live scan. x402 clients cap a payment at US$1 by default; raise their limit to pay more at once |
| `rotate_key` | boolean | no | Make a new key and revoke the current one. Needs `Authorization: Bearer <the current key>` |

### AgentCredit

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `object` | string: "agent_credit" | yes |  |
| `network` | string: "eip155:8453" \| "eip155:84532" \| "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" \| "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1" | yes |  |
| `payer` | string (pattern ^(0x[0-9a-f]{40}\|[1-9A-HJ-NP-Za-km-z]{32,44})$) | yes | Lowercase on Base; base58 as written on Solana |
| `transaction` | string (pattern ^(0x[0-9a-f]{64}\|[1-9A-HJ-NP-Za-km-z]{64,88})$) | yes | The transaction hash (Base) or signature (Solana) |
| `livemode` | boolean | yes | true on mainnet: live credit and a live key |
| `amount_cents` | integer | yes |  |
| `balance_cents` | integer \| null | yes | The wallet after this payment; null on testnet (nothing is credited) |
| `api_key` | string (pattern ^sc_(live\|test)_) | no | Only when the key was created by this call: store it, it is not shown again |
| `api_key_prefix` | string \| null | yes | The account's key of this mode; null when it has none |
| `notice` | string | yes | What to do with the key (store it now, or use the one stored) |
| `docs` | string (uri) | yes |  |

### X402PaymentRequirements

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `scheme` | string: "exact" | yes |  |
| `network` | string: "eip155:8453" \| "eip155:84532" \| "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" \| "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1" | yes | CAIP-2: Base (`eip155:…`) or Solana (`solana:…`) |
| `amount` | string (pattern ^\d+$) | yes | Atomic units of the asset (USDC: 6 decimals) |
| `asset` | string (pattern ^(0x[0-9a-fA-F]{40}\|[1-9A-HJ-NP-Za-km-z]{32,44})$) | yes | The USDC contract (Base) or mint (Solana) |
| `payTo` | string (pattern ^(0x[0-9a-fA-F]{40}\|[1-9A-HJ-NP-Za-km-z]{32,44})$) | yes | Who is paid: on Solana, the owner of the USDC account the transfer goes to |
| `maxTimeoutSeconds` | integer | yes |  |
| `extra` | object \| object | yes |  |

### X402PaymentRequired

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `x402Version` | integer: "2" | yes |  |
| `error` | string | no |  |
| `resource` | object | yes |  |
| `accepts` | array of X402PaymentRequirements | yes |  |
| `extensions` | object | no | `bazaar`: how to call this resource and what it returns, for x402 catalogs |

### AgentPaymentError

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `error` | object | yes |  |

### Error

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `error` | object | yes |  |
