Skip to content
API / API reference
  1. Docs
  2. API
  3. 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.

Open .md

Base URL https://api.spacecontext.app/v1 · 29 operations · 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" } }: see Errors.

Machine-readable contract: openapi.json · typed client: @spacecontext/sdk

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

API key

Parameters

NameIn · typeDescription
limit query · integer (≥ 1, ≤ 50)
cursor query · string
source query · string: "app" | "api" 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
{
  "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

API key

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.

Request body application/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
{
  "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"
}
Example 201 response
{
  "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

API key

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.

Parameters

NameIn · typeDescription
idrequired path · string (pattern ^(app|spc)_)
include query · string: "transcript" Adds the transcript. Requires the spaces:sensitive scope; without it the request is refused with 403 INSUFFICIENT_SCOPE.
enrich query · string: "auto" | "none"

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
{
  "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

API key

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

Parameters

NameIn · typeDescription
idrequired path · string (pattern ^(app|spc)_)

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

API key

Parameters

NameIn · typeDescription
idrequired path · string (pattern ^(app|spc)_)

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
{
  "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

API key

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.

Parameters

NameIn · typeDescription
idrequired path · string (pattern ^(app|spc)_)
Idempotency-Key header · string (pattern ^[A-Za-z0-9_-]{8,64}$)

Request body application/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
{
  "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"
}
Example 201 response
{
  "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

API key

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.

Parameters

NameIn · typeDescription
idrequired path · string (pattern ^(app|spc)_)
scan_idrequired path · string (pattern ^scn_)
include query · string: "transcript" 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
{
  "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

API key

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.

Parameters

NameIn · typeDescription
idrequired path · string (pattern ^(app|spc)_)
include query · string: "transcript" Adds references/transcript.md. Requires the spaces:sensitive scope; without it the request is refused with 403 INSUFFICIENT_SCOPE.
enrich query · string: "auto" | "none"

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
{
  "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

API key

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.

Parameters

NameIn · typeDescription
idrequired path · string (pattern ^(app|spc)_)
include query · string: "transcript" Adds the transcript. Requires the spaces:sensitive scope; without it the request is refused with 403 INSUFFICIENT_SCOPE.
enrich query · string: "auto" | "none"

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

API key

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.

Parameters

NameIn · typeDescription
idrequired path · string (pattern ^(app|spc)_)

Request body application/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
{
  "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

API key

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.

Parameters

NameIn · typeDescription
idrequired path · string (pattern ^(app|spc)_)
fromrequired query · string (pattern ^scn_) Id of the earlier scan.
to query · string (pattern ^scn_) 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
{
  "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

API key

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.

Parameters

NameIn · typeDescription
idrequired path · string (pattern ^scn_[0-9a-f]{32}$)

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
{
  "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

API key

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

Parameters

NameIn · typeDescription
idrequired path · string (pattern ^scn_[0-9a-f]{32}$)

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

API key

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.

Parameters

NameIn · typeDescription
idrequired path · string (pattern ^scn_[0-9a-f]{32}$)

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

API key

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.

Parameters

NameIn · typeDescription
idrequired path · string (pattern ^scn_[0-9a-f]{32}$)
indexrequired path · integer (≥ 0, ≤ 119)

Request body image/jpeg, image/png, image/webp

The raw bytes of the file.

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
{
  "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

API key

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.

Parameters

NameIn · typeDescription
idrequired path · string (pattern ^scn_[0-9a-f]{32}$)

Request body video/mp4, video/quicktime, video/x-m4v, video/3gpp, video/webm, video/x-matroska, video/matroska

The raw bytes of the file.

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
{
  "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

API key

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.

Parameters

NameIn · typeDescription
idrequired path · string (pattern ^scn_[0-9a-f]{32}$)

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
{
  "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"
}
Example 202 response
{
  "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

API key

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).

Parameters

NameIn · typeDescription
space_type query · string: "apartment" | "house" | "office" | "warehouse" | "vehicle" | "boat" | "camper" | "shop" | "garage" | "appliance" | "other" The type of space being filmed: its key locations, and the area question.

Request body image/jpeg, image/png, image/webp

The raw bytes of the file.

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
{
  "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

No auth

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=.

Parameters

NameIn · typeDescription
language query · string: "en" | "it" | "es" | "fr" | "de" | "pt" | "nl" 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
{
  "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

No auth

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).

Parameters

NameIn · typeDescription
space_typerequired path · string: "apartment" | "house" | "office" | "warehouse" | "vehicle" | "boat" | "camper" | "shop" | "garage" | "appliance" | "other"
language query · string: "en" | "it" | "es" | "fr" | "de" | "pt" | "nl" 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
{
  "object": "capture_profile",
  "space_type": "apartment",
  "language": "en",
  "key_locations": [
    {
      "kind": "electrical_panel",
      "label": "…",
      "hint": "string",
      "expected": true
    }
  ],
  "areas": [
    {
      "value": "entrance",
      "label": "…"
    }
  ]
}

Media

Evidence frames, served through signed links.

GET /media/{key}

Serve a signed v1 evidence frame

No auth

Parameters

NameIn · typeDescription
keyrequired path · string (pattern ^(frames|api)/) 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.
tokenrequired query · string
expiresrequired query · integer

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

API key

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.

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
{
  "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

API key

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
{
  "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)

No auth

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.

Request body application/json · optional

{
  "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
{
  "device_code": "string",
  "user_code": "BCDF-2345",
  "verification_url": "string",
  "expires_in": 1,
  "interval": 1
}
POST /auth/device/token

Poll a device sign-in

No auth

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.

Request body application/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
{
  "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

No auth

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

Responses

200 API metadata · ApiInfo
Example 200 response
{
  "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

No auth

Responses

200 OpenAPI document

Other

POST /agent/credits

Pay with x402 and get an API key (agents)

No auth

For an agent with a wallet and no account: pay in USDC on Base or Solana with x402 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.

Parameters

NameIn · typeDescription
PAYMENT-SIGNATURE header · string (max 16384) 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 · optional

{
  "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
{
  "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 shapes most answers are made of. Every schema of the contract is in openapi.json and exported as a type by the SDK.

SpaceDocument
FieldTypeDescription
objectrequired string: "space_document"
schema_versionrequired string: "1.0"
spacerequired object
scanrequired object
enrichmentrequired object
summaryrequired string | null
confidencerequired number | null (≥ 0, ≤ 1)
statsrequired object
featuresrequired array of string
areasrequired array of SpaceArea
itemsrequired array of SpaceItem
issuesrequired array of SpaceIssue
key_locationsrequired array of SpaceKeyLocation
instructionsrequired array of SpaceInstruction
factsrequired array of SpaceFact
qarequired array of SpaceQA
transcript object
framesrequired array of SpaceFrame
continuityrequired SpaceContinuity | null
coveragerequired object
SpaceSummary
FieldTypeDescription
objectrequired string: "space"
idrequired string (pattern ^(app|spc)_)
sourcerequired string: "app" | "api"
namerequired string
space_typerequired SpaceType
created_atrequired string (date-time)
updated_atrequired string (date-time)
latest_scanrequired object | null
Scan
FieldTypeDescription
objectrequired "scan"
idrequired string (pattern ^scn_[0-9a-f]{32}$)
space_idrequired string
livemoderequired boolean
statusrequired string: "awaiting_upload" | "queued" | "processing" | "complete" | "failed" | "cancelled"
stagerequired string | null: "ingesting" | "screening" | "observing" | "fusing" | "enriching" | "saving" | "verifying" | "null"
inputrequired object
chargerequired object
errorrequired object | null
uploadrequired object | null
created_atrequired string | null (date-time)
started_atrequired string | null (date-time)
completed_atrequired string | null (date-time)
ScanSummary
FieldTypeDescription
objectrequired string: "scan"
idrequired string (pattern ^scn_)
space_idrequired string (pattern ^(app|spc)_)
analyzed_atrequired string | null (date-time)
areasrequired integer
itemsrequired integer
issuesrequired integer
currentrequired boolean
SearchResults
FieldTypeDescription
objectrequired "search_results"
queryrequired string
datarequired array of object
SpaceAnswer
FieldTypeDescription
objectrequired "answer"
space_idrequired string
scan_idrequired string
questionrequired string
answerrequired string
documentedrequired boolean
citationsrequired array of object
SpaceDiff
FieldTypeDescription
objectrequired "space_diff"
space_idrequired string
fromrequired object
torequired object
levelrequired string: "base" | "deep"
linkedrequired boolean 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.
summaryrequired object
areasrequired object
itemsrequired object
issuesrequired object
SkillBundle
FieldTypeDescription
objectrequired "skill"
namerequired string (max 64, pattern ^[a-z0-9]+(-[a-z0-9]+)*$)
space_idrequired string
scan_idrequired string
filesrequired array of SkillFile
FrameCheck
FieldTypeDescription
objectrequired string: "frame_check"
livemoderequired boolean
space_typerequired string | null: "apartment" | "house" | "office" | "warehouse" | "vehicle" | "boat" | "camper" | "shop" | "garage" | "appliance" | "other" | "null" The space_type asked, or null.
usablerequired boolean Sharp, lit and showing the space: worth keeping in a scan.
qualityrequired object
shows_spacerequired number (≥ 0, ≤ 1) Probability that the frame shows the real thing being filmed (a place, a vehicle, a boat, an appliance), not a drawing, document or screen.
arearequired object | null 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_locationsrequired array of object Every key location of the type of space, in its order.
Wallet
FieldTypeDescription
objectrequired "wallet"
currencyrequired "usd"
balance_centsrequired integer
free_scans_remainingrequired integer (≥ 0)
scan_price_centsrequired integer (≥ 1)
card_on_filerequired boolean 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 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_rechargerequired object
terms_accepted object | null 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.

Space Context docs · llms.txt · download Edit on GitHub

esc

↑↓ navigate↵ openesc close