Skip to main content

Scans

A scan goes through four calls: create, upload, start, then follow it (polling or webhooks).

API=https://api.spacecontext.finikos.it/v1
AUTH="Authorization: Bearer $SPACECONTEXT_API_KEY"

1. Create a space​

curl -X POST $API/spaces -H "$AUTH" -H "Content-Type: application/json" \
-d '{"name": "Seaside flat", "space_type": "apartment"}'
{ "object": "space", "id": "spc_3f…", "source": "api", "livemode": true, "name": "Seaside flat", "space_type": "apartment", "description": null, "external_id": null, "created_at": "…", "updated_at": "…" }

external_id (up to 128 characters) lets you store your own reference, for example a listing id.

2. Declare the scan​

For a video, declare its type and exact size:

curl -X POST $API/spaces/spc_3f…/scans -H "$AUTH" -H "Content-Type: application/json" \
-H "Idempotency-Key: flat-2026-10-05" \
-d '{"input": {"kind": "video", "language": "en", "video": {"content_type": "video/mp4", "content_length": 48211983}}}'

For photos, list them in order, with their time in milliseconds when you know it:

{ "input": { "kind": "frames", "language": "it", "frames": [{ "timestamp_ms": 0 }, { "timestamp_ms": 2000 }] } }

Either can carry a transcript ({ "text": "…", "segments": [{ "start_ms": 0, "end_ms": 1500, "text": "This is the kitchen." }] }) when you already have one; for videos without it, the audio is transcribed for you.

The answer is the scan, with upload instructions:

{
"object": "scan", "id": "scn_8a…", "status": "awaiting_upload",
"upload": {
"kind": "video", "method": "PUT",
"url": "https://…",
"headers": { "Content-Type": "video/mp4", "Content-Length": "48211983" },
"expires_at": "…"
}
}

3. Upload​

Video: PUT the bytes to upload.url with upload.headers. When the URL points to storage (a signed link, valid for an hour), do not send your API key. When it points to the API (…/v1/scans/{id}/video, up to 95 MB), send the Authorization header as usual.

curl -X PUT "$UPLOAD_URL" -H "Content-Type: video/mp4" --data-binary @flat.mp4

Photos: one PUT per photo to upload.url_template with {index} replaced, starting at 0:

curl -X PUT $API/scans/scn_8a…/frames/0 -H "$AUTH" -H "Content-Type: image/jpeg" --data-binary @frame-0.jpg

Uploads can be repeated. A scan not started within 24 hours is cancelled and its uploads deleted.

4. Start​

curl -X POST $API/scans/scn_8a…/start -H "$AUTH" -H "Content-Type: application/json" -d '{}'

202 with the scan queued and its charge: { "kind": "free_scan" | "scan_charge", "price_cents": 100, "refunded": false }. The scan is charged now, atomically; see Billing.

AnswerMeaning
202Started
200Already started; nothing charged again
409 UPLOAD_INCOMPLETESome photos or the video are missing
429 SCANS_IN_PROGRESSThree scans are already processing; start again when one finishes
402 INSUFFICIENT_FUNDSAdd funds (automatic top-up, if enabled, starts now)
402 SPENDING_CAP_REACHEDThe key's monthly cap is reached

5. Follow​

curl $API/scans/scn_8a… -H "$AUTH"

status moves through queued, processing (with stage: ingesting, observing, fusing, enriching, saving) to complete, or failed with error and charge.refunded: true. Poll every few seconds, or subscribe to scan.completed and scan.failed.

Then read the space: GET /spaces/spc_3f… returns its Space Document.

Cancel​

DELETE /scans/{id} cancels a scan still awaiting_upload (204). Nothing is charged and its uploads are deleted.

With the SDK​

import { SpaceContext } from '@spacecontext/sdk';
import { openAsBlob } from 'node:fs';

const sc = new SpaceContext();
const space = await sc.spaces.create({ name: 'Seaside flat', space_type: 'apartment' });
const scan = await sc.scans.fromVideo(space.id, { data: await openAsBlob('flat.mp4'), contentType: 'video/mp4' });
const done = await sc.scans.wait(scan.id);