Turn 360° photos into a hosted, shareable virtual tour with one call. AI names the rooms and places the navigation points between them; you get a public tour URL and an embed iframe.
POST your 360° photos, get back a hosted virtual tour you can share or embed. Pedra imports the equirectangular photos, AI names every room and places the door-to-door navigation points, and the response gives you a public tourUrl and a ready-to-paste embedCode iframe. No viewer to host and no navigation points to place by hand.
What you get from one call:
A hosted tour page — a public tourUrl plus an embedCode iframe for any listing page, CRM or portal. Works on phones (with the gyroscope) and on desktop.
AI room names — rooms you don't name are named for you (Entrance, Kitchen, Bedroom 1…) in the tour's language, for free. Names you pass are never changed.
AI navigation — Pedra finds where each doorway is in the photo and places the navigation points between rooms, in both directions.
Full control afterwards — rename, reorder, remove or add rooms, and replace the navigation points with your own, all through the API.
Works from chat — the same endpoints are tools in the Pedra MCP server, so ChatGPT and Claude can build a tour from a conversation.
How it works
Building a tour is asynchronous: the create call returns a tourId straight away and the tour builds in the background (roughly 10 seconds per linked room).
Create — POST /api/create_virtual_tour with your 360° photos in walking order. You get the tourId, tourUrl and embedCode immediately, with status: "processing".
Poll — call POST /api/get_virtual_tour every few seconds until status is "ready" (or "failed", with the reason in error).
Share — send the tourUrl, or paste the embedCode into your listing page. The URL stays the same when you edit the tour later.
# 1. Create the tour (returns immediately with "status": "processing")
curl -X POST https://app.pedra.ai/api/create_virtual_tour \
-H "Content-Type: application/json" \
-d '{
"apiKey": "YOUR_API_KEY",
"name": "Calle Mayor 12",
"scenes": [
{ "imageUrl": "https://example.com/360/entrance.jpg", "name": "Entrance" },
{ "imageUrl": "https://example.com/360/living-room.jpg" },
{ "imageUrl": "https://example.com/360/kitchen.jpg" },
{ "imageUrl": "https://example.com/360/bedroom.jpg" }
]
}'
# 2. Poll until "status" is "ready" (or "failed")
curl -X POST https://app.pedra.ai/api/get_virtual_tour \
-H "Content-Type: application/json" \
-d '{ "apiKey": "YOUR_API_KEY", "tourId": "TOUR_ID" }'
Linking modes & credits
The linking parameter decides how navigation points are placed. Room naming is always free. Credits are charged only when linking starts, and a build that fails is never charged.
Mode
What it does
Cost
sequential
Default. Pass rooms in walking order; each room is linked to the next, in both directions.
max(3, ⌈rooms ÷ 3⌉) credits — 3 up to 9 rooms, 4 for 12, 10 for 30
smart
AI compares every pair of rooms and links the ones that visibly connect — for when you don't know the walking order. Slower; up to 40 rooms.
5–160 credits by room count (table below)
none
No navigation points. Place your own later with update_virtual_tour.
Free
Smart linking by room count
Rooms
Credits
1–6
5 credits
7–10
12 credits
11–15
25 credits
16–20
50 credits
21–25
70 credits
26–30
100 credits
31–35
130 credits
36–40
160 credits
add_virtual_tour_scenes costs the same as sequential linking, counting only the new rooms. get_virtual_tour, list_virtual_tours, update_virtual_tour, delete_virtual_tour and create_upload_link are free. Check your balance with credits.
Create a virtual tour
POST/api/create_virtual_tour
Imports the 360° photos, then names and links the rooms in the background. Pass scenes in walking order — the first one is where the tour opens. Without a propertyId, a new property is created, named after name. To build a tour from photos already in a property (for example ones uploaded through an upload link), pass only the propertyId and leave out scenes: every 360° photo in the property is used, in upload order. A property has one tour; if it already has one you get 409 tour_exists with its tourId.
apiKeystringrequired
Your API key.
scenesarray
The 360° photos, in walking order (max 50). Required unless you pass a propertyId whose 360° photos should become the tour.
imageUrlsarray
Shorthand for scenes: a plain list of 360° photo URLs.
propertyIdstring
Build the tour in this property. Needed when scenes use imageId. Without scenes, all of the property's 360° photos are used in upload order. Omit to create a new property.
namestring
Tour title, e.g. the listing address. Also the new property's name when one is created.
linkingstring
How navigation points are placed — see Linking modes & credits.
Values:sequentialsmartnone
Default:sequential
languagestring
Language of the tour page and of the AI room names. Non-English tours get a URL like /es/virtual-tour/….
Values:enesfrdeitpt
Default:en
Scene object
Each scene is { imageUrl } or { imageId }, with an optional name. A plain string is treated as an imageUrl. Up to 50 scenes per tour.
imageUrlstring
Public URL (or data: URI) of an equirectangular 360° photo — 2:1, up to 80 MB. Dropbox and Google Drive share links work.
imageIdstring
A 360° photo already in the property (from list_property_images with type "360"). Use instead of imageUrl.
Returns the tour's status and, once it's ready, its scenes, navigation links and display settings. This is the endpoint to poll after create_virtual_tour and add_virtual_tour_scenes.
apiKeystringrequired
Your API key.
tourIdstringrequired
The tour's id (from create_virtual_tour or list_virtual_tours).
processing — building. progress.stage is queued, importing, naming or linking, with done / total where it applies.
ready — live at tourUrl. Includes scenes, links and settings.
failed — nothing was charged. error says why and failedScenes lists each photo that couldn't be used (for example one that isn't 2:1). Photos that did import stay in the property, so you can replace the bad one and create the tour again by imageId.
If an add_virtual_tour_scenes call fails, the tour stays ready and carries lastError and failedScenes. On a free-plan account the tour builds, but its public link shows an upgrade page: responses then say shareable: false, with a shareableNote.
Returns the account's tours, newest first (up to 100), with the same summary fields as get_virtual_tour but without scenes, links and settings. Deleted tours are left out.
Rename the tour or its rooms, reorder or remove rooms, replace the navigation points, and change how they look. Free and instant. Validation is all-or-nothing: if any field is invalid, nothing changes. Returns the full tour, like get_virtual_tour, or 409 tour_processing while the tour is still building.
apiKeystringrequired
Your API key.
tourIdstringrequired
The tour's id (from create_virtual_tour or list_virtual_tours).
namestring
New tour title.
sceneNamesobject
Room names by sceneId, e.g. { "<sceneId>": "Kitchen" }.
sceneOrderarray
Every sceneId exactly once, in the new order. The first is where the tour opens.
removeScenesarray
sceneIds to take out of the tour. The photos stay in the property and links touching them are dropped. At least one scene must remain.
linksarray
Replaces ALL navigation links. Each link goes one way: { fromSceneId, toSceneId, yaw, pitch? }.
navigationStylestring
Colour of the navigation points.
Values:whiteblue
navigationSizestring
Size of the navigation points.
Values:smallmediumlarge
showLabelsboolean
Always show the destination room's name next to each navigation point.
languagestring
Tour page language.
Values:enesfrdeitpt
Link object
fromSceneIdstringrequired
The scene the navigation point is shown in.
toSceneIdstringrequired
The scene it leads to.
yawnumberrequired
Horizontal angle in the fromScene photo, −180 to 180. 0 is the centre of the photo; negative is to the left.
pitchnumber
Vertical angle, −90 to 90. 0 is the horizon.
Default:0
To adjust the AI's links rather than start from scratch, read links from get_virtual_tour, edit the list and send it back.
Adds 360° photos to an existing tour. With sequential linking only the new stretch is linked: the last existing room to the first new room, then each new room to the next. Existing links, including ones you placed yourself, are kept. Costs max(3, ⌈new rooms ÷ 3⌉) credits. Asynchronous like create, and the tour stays live meanwhile — poll get_virtual_tour until status is "ready" again.
apiKeystringrequired
Your API key.
tourIdstringrequired
The tour's id (from create_virtual_tour or list_virtual_tours).
scenesarrayrequired
The 360° photos to add, same shape as in create_virtual_tour. imageId scenes must be 360° photos in the tour's property.
linkingstring
sequential links the new stretch; none adds the rooms without navigation points.
{
"tourId": "1d7aabf8-3c2e-4b8a-9f61-0c5d2e7a4b19",
"propertyId": "196e742a-5b0d-4c9e-8a3f-7e2b1c6d9f04",
"name": "Calle Mayor 12",
"status": "processing",
"tourUrl": "https://app.pedra.ai/virtual-tour/1d7aabf8-3c2e-4b8a-9f61-0c5d2e7a4b19",
"sceneCount": 6,
"linkCount": 6,
"progress": { "stage": "queued" },
"addedScenes": [
{ "sceneId": "3e7b1a95-4c0f-4b2d-9e68-1a5d8c3f7b02", "name": "Bathroom", "source": "https://example.com/360/bathroom.jpg" },
{ "sceneId": "9f6c2d48-7a1e-4e5b-8c3d-0b4a6f9e2d17", "name": null, "source": "https://example.com/360/terrace.jpg" }
],
"creditsCost": 3,
"estimatedSeconds": 31,
"message": "Adding the scenes. The tour stays live while this runs. Poll get_virtual_tour until status is \"ready\"."
}
Delete a virtual tour
POST/api/delete_virtual_tour
Deletes the tour. Its public link returns 404 from then on; the 360° photos stay in the property, so you can build a new tour from them. Deleting is API-only — it isn't exposed as an MCP tool.
apiKeystringrequired
Your API key.
tourIdstringrequired
The tour's id (from create_virtual_tour or list_virtual_tours).
{
"message": "Virtual tour deleted. Its 360° photos are still in the property.",
"tourId": "1d7aabf8-3c2e-4b8a-9f61-0c5d2e7a4b19"
}
Create an upload link
POST/api/create_upload_link
Returns a no-login upload page for one property, valid for 24 hours. Send it to whoever has the photos — the photographer, the agent, the owner. They open it on a phone or a computer, drop the photos in (up to 100 files, JPEG, PNG or WebP, up to 80 MB each; iPhone HEIC photos work when uploaded from the iPhone itself) and see thumbnails, progress and an "All set" when they're done. Files are sorted by file name, which on 360° cameras is the shooting order — usually the walking order too.
type sets what the page takes:
"any" (default) — regular photos and 360° photos. 2:1 images are detected and stored as 360° photos automatically. Use it for photos to edit, stage or turn into a video, or a mix.
"360" — only 360° photos; anything else is rejected with a clear message. Use it when the link is for a virtual tour.
Photos are stored the way the web app stores them: regular photos at 2048 px on the long side (3072 px when the file is over 20 MB), 360° photos 4096 px wide (6144 px over 20 MB).
It's also how a chat assistant gets photos off a phone: Claude can't pass chat attachments to a tool, and photos still on a phone aren't in the chat at all, so the assistant creates an upload link, you upload, and it carries on. (In ChatGPT, photos attached to the chat work directly — see MCP server.) The link can only add photos to that one property — it gives no other access to the account. When the upload is done, call list_property_images (type"photo" or "360") for URLs to edit or turn into a video, or create_virtual_tour with the propertyId and no scenes.
Limits: 5 upload links a day on the free plan, 50 on paid plans (429 upload_link_limit) — each link works for 24 hours, so reuse today's instead of making a new one. Every file uploaded through a link also counts toward the daily upload limit.
apiKeystringrequired
Your API key.
propertyIdstring
The property the photos go into. Omit to create a new property named after name.
namestring
Name of the new property, e.g. the listing address. Shown on the upload page.
typestring
What the page takes: "any" for photos and 360° photos (2:1 images are stored as 360° automatically), or "360" for 360° photos only — use it for a virtual tour.
{
"uploadUrl": "https://app.pedra.ai/upload/q7Ht2vXk9LmP4wRz8NcB1sYd6FgJ3eUa0KoV5iTn?lang=es",
"propertyId": "196e742a-5b0d-4c9e-8a3f-7e2b1c6d9f04",
"propertyName": "Calle Mayor 12",
"type": "360",
"expiresAt": "2026-10-01T15:26:30.687Z",
"maxFiles": 100,
"appUrl": "https://app.pedra.ai/?projectId=196e742a-5b0d-4c9e-8a3f-7e2b1c6d9f04",
"message": "Send this link to whoever has the 360° photos. It works on a phone or a computer with no login, for 24 hours. When they're done, list the property's photos (list_property_images; type "360" for 360° photos) and use them."
}
Uploading 360° photos
Tours are built from equirectangular 360° photos — the 2:1 panoramas that 360° cameras export. Each photo is checked to be 2:1 (1.9–2.1 is accepted), up to 80 MB, and stored as a JPEG 4096 px wide (6144 px when the file is over 20 MB). There are three ways to get them in:
Public URLs, directly — pass them as scenes to create_virtual_tour. Best when the photos are already online (your CDN or storage bucket). Dropbox and Google Drive share links are converted to direct downloads for you.
add_images_to_property with type: "360" — store the photos in a property first (up to 10 per call), then call create_virtual_tour with just the propertyId. It also accepts data: URIs, so you can send local files base64-encoded — one or a few per call, under the 50 MB request limit.
An upload link — when the photos are on someone's phone or camera rather than online. See create_upload_link with type: "360".
Sending local 360° files as data: URIs, two per call, then building the tour:
import base64
import pathlib
import requests
API = "https://app.pedra.ai/api"
API_KEY = "YOUR_API_KEY"
def data_uri(path):
return "data:image/jpeg;base64," + base64.b64encode(path.read_bytes()).decode()
# Sorted by file name = shooting order on most 360° cameras
files = sorted(pathlib.Path("./calle-mayor-12").glob("*.jpg"))
prop = requests.post(f"{API}/create_property",
json={"apiKey": API_KEY, "name": "Calle Mayor 12"}).json()
for i in range(0, len(files), 2): # two per call keeps the body under 50 MB
res = requests.post(f"{API}/add_images_to_property", json={
"apiKey": API_KEY,
"propertyId": prop["propertyId"],
"type": "360",
"imageUrls": [data_uri(p) for p in files[i:i + 2]],
}).json()
for f in res["failed"]:
print("Skipped:", f["error"])
# No scenes: every 360° photo in the property, in upload order
tour = requests.post(f"{API}/create_virtual_tour",
json={"apiKey": API_KEY, "propertyId": prop["propertyId"]}).json()
Errors
Errors come back as { "error": "...", "code": "..." } (code only where listed). See Errors & limits for the general model.
400 — invalid input: a photo that isn't 360°, more than 50 scenes, an unknown linking value. no_panoramas: the property has no 360° photos yet. no_new_scenes: every scene you're adding is already in the tour.
402 insufficient_credits — with creditsRequired, creditsAvailable and upgradeUrl. Nothing is created or charged.
404 — User not found (bad API key), Virtual tour not found, or Property not found for this account.
409 — tour_exists: the property already has a tour (its tourId is in the response). tour_processing: the tour is still building — wait until it's ready. tour_failed: the build failed — delete the tour and create it again.
429 — upload_limit: the photos passed by URL would go over the account's daily upload limit (30 a day on the free plan, 500 on paid plans, reset at midnight UTC); the whole call is refused and nothing is stored. upload_link_limit: the account has made its upload links for today.
Next
Build tours from ChatGPT or Claude with the MCP server, manage the photos behind a tour with the Properties API, or turn the same listing into a listing video.