Your First Change, End to End
This page is one linear script, from a fresh access token to a change you can see in the Prevu3D Platform, and back. It uses the Client Credentials flow throughout, and every ID it needs comes from an API response earlier in the same script: nothing to fill in by hand.
What you’ll do
Section titled “What you’ll do”- Get an access token: authenticate with Client Credentials
- Discover your organization: call
/oauth/api-infofor your API URL and organization ID - Browse to a division: the first child of your organization
- Browse to a site: the first child of that division
- Rename the site: one small, obvious, reversible change
- Confirm it in the Prevu3D Platform: the finish line
- Undo the rename: leave no debris behind
Prerequisites
Section titled “Prerequisites”- Complete the Client Credentials Flow guide first. Your OAuth application needs the
read:hierarchyandwrite:hierarchyscopes, and your service user needs content access and an Edit-level role on at least one site (see the security model). - Your organization needs at least one Division containing at least one Site. This walkthrough browses to the first one it finds.
Step 1: Get an access token
Section titled “Step 1: Get an access token”Endpoint: POST https://cloud-api.prevu3d.com/oauth/token
POST https://cloud-api.prevu3d.com/oauth/token HTTP/1.1Host: cloud-api.prevu3d.comAuthorization: Basic eW91ci1jbGllbnQtaWQ6eW91ci1jbGllbnQtc2VjcmV0Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials{ "hasError": false, "access_token": "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9...", "refresh_token": "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9...", "expires_in": 3599, "token_type": "bearer"}Save access_token. See the Client Credentials Flow guide if this fails.
Step 2: Discover your organization
Section titled “Step 2: Discover your organization”Endpoint: GET https://cloud-api.prevu3d.com/oauth/api-info
Headers: Authorization: Bearer <access_token>
{ "organization": { "id": "217ebd23-ec54-4af0-a6d6-4a441a6d1966", "name": "Test Organization" }, "scopes": ["read:hierarchy", "write:hierarchy"], "apiUrl": "https://api-ue1.prevu3d.com/realityconnect-api"}Save apiUrl and organization.id. Every request below uses {apiUrl}.
Step 3: Browse to a division
Section titled “Step 3: Browse to a division”Endpoint: GET {apiUrl}/v1/nodes/{organization_id}/browse
{ "node": { "id": "217ebd23-ec54-4af0-a6d6-4a441a6d1966", "type": "Organization", "...": "..." }, "items": [ { "id": "3d3b6f2a-9e0a-4b0a-8b0a-1c2d3e4f5a6b", "name": "North America Operations", "type": "Division", "...": "..." } ], "total": 1}Take items[0].id as division_id.
Step 4: Browse to a site
Section titled “Step 4: Browse to a site”Endpoint: GET {apiUrl}/v1/nodes/{division_id}/browse
{ "node": { "id": "3d3b6f2a-9e0a-4b0a-8b0a-1c2d3e4f5a6b", "type": "Division", "...": "..." }, "items": [ { "id": "8a1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e", "name": "Chicago Plant", "type": "Site", "...": "..." } ], "total": 1}Take items[0].id as site_id, and keep items[0].name ("Chicago Plant" here); you’ll need it in Step 7.
Step 5: Rename the site
Section titled “Step 5: Rename the site”Renaming a node is a small, obvious, reversible change: it needs write:hierarchy, touches nothing else, and takes one call to reverse.
Endpoint: PATCH {apiUrl}/v1/nodes/{site_id}
PATCH https://api-ue1.prevu3d.com/realityconnect-api/v1/nodes/8a1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e HTTP/1.1Authorization: Bearer <access_token>Content-Type: application/json
{ "name": "Chicago Plant (test)" }{ "id": "8a1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e", "name": "Chicago Plant (test)", "type": "Site", "...": "..." }Step 6: Confirm it in the Prevu3D Platform
Section titled “Step 6: Confirm it in the Prevu3D Platform”Open the Prevu3D Platform, navigate to the division from Step 3, and open the site from Step 4.
You should now see the new name, Chicago Plant (test) in this example, at the top of the site’s page and in the division’s list of sites. That’s the finish line: a change made entirely through the API, visible in the product.
Step 7: Undo the rename
Section titled “Step 7: Undo the rename”Endpoint: PATCH {apiUrl}/v1/nodes/{site_id}
{ "name": "Chicago Plant" }Send the site’s original name from Step 4. The response, and the Platform, now show the site as it was before Step 5.
Try it with Python
Section titled “Try it with Python”This script runs Steps 1–7 in order. It pauses after the rename so you can check the Prevu3D Platform before it undoes the change.
import requestsimport base64
client_id = "your-client-id"client_secret = "your-client-secret"base_url = "https://cloud-api.prevu3d.com"
# Step 1: Get a tokentoken_response = requests.post( f"{base_url}/oauth/token", data={"grant_type": "client_credentials"}, headers={ "Authorization": f'Basic {base64.b64encode(f"{client_id}:{client_secret}".encode()).decode()}', "Content-Type": "application/x-www-form-urlencoded", },)token_response.raise_for_status()access_token = token_response.json()["access_token"]headers = {"Authorization": f"Bearer {access_token}"}
# Step 2: Discover your API URL and organization IDapi_info = requests.get(f"{base_url}/oauth/api-info", headers=headers).json()api_url = api_info["apiUrl"]organization_id = api_info["organization"]["id"]
# Step 3: Browse to the first divisiondivisions = requests.get(f"{api_url}/v1/nodes/{organization_id}/browse", headers=headers).json()division_id = divisions["items"][0]["id"]
# Step 4: Browse to the first site in that divisionsites = requests.get(f"{api_url}/v1/nodes/{division_id}/browse", headers=headers).json()site = sites["items"][0]site_id = site["id"]original_name = site["name"]
# Step 5: Rename the sitenew_name = f"{original_name} (test)"requests.patch(f"{api_url}/v1/nodes/{site_id}", json={"name": new_name}, headers=headers).raise_for_status()print(f"Renamed site {site_id} to '{new_name}'.")print("Open the site in the Prevu3D Platform now. You should see the new name.")input("Press Enter once you've confirmed it, to undo the change...")
# Step 7: Undo the renamerequests.patch(f"{api_url}/v1/nodes/{site_id}", json={"name": original_name}, headers=headers).raise_for_status()print(f"Restored site {site_id} to '{original_name}'.")Production-ready version
Section titled “Production-ready version”The walkthrough above is a happy path: a fresh token, a quiet network, nothing to retry. A script that keeps running — a bulk import, a polling loop, a large download — eventually meets a 429, an access token that expires mid-run, or a single call that needs a retry while the rest of the batch keeps going. Rather than growing every example on this site with the same defensive code, this section introduces one small reusable client once; the examples that need it link back here instead of repeating it.
RCAPIClient wraps the raw requests calls above with three things: it caches the access token and refreshes it automatically — proactively before it expires, and reactively on a 401 — it retries a 429 using the Retry-After header (see Rate Limits) instead of guessing a delay, and it gives up after a maximum number of attempts or a maximum elapsed time instead of retrying forever.
import timeimport base64import requests
class RCAPIClient: """Token cache + refresh, retry with backoff, and a give-up condition.
Wrap the raw `requests` calls above with this once your integration runs long enough to hit a rate limit or outlive an access token -- most bulk imports, polling loops, and long-running downloads do. """
def __init__(self, client_id, client_secret, cloud_api_base="https://cloud-api.prevu3d.com"): self.client_id = client_id self.client_secret = client_secret self.cloud_api_base = cloud_api_base self._access_token = None self._expires_at = 0 # epoch seconds
def _fetch_token(self): credentials = base64.b64encode(f"{self.client_id}:{self.client_secret}".encode()).decode() response = requests.post( f"{self.cloud_api_base}/oauth/token", data={"grant_type": "client_credentials"}, headers={"Authorization": f"Basic {credentials}"}, ) response.raise_for_status() body = response.json() self._access_token = body["access_token"] # Refresh a little before the real expiry, not exactly at it. self._expires_at = time.time() + body["expires_in"] - 60
def _token(self): if self._access_token is None or time.time() >= self._expires_at: self._fetch_token() return self._access_token
def request(self, method, url, max_attempts=6, max_elapsed=300, **kwargs): """`requests.request()` plus token refresh, 429 backoff, and a give-up condition.""" start = time.time() headers = kwargs.pop("headers", {}) for attempt in range(1, max_attempts + 1): headers["Authorization"] = f"Bearer {self._token()}" response = requests.request(method, url, headers=headers, **kwargs)
if response.status_code == 401 and attempt == 1: self._access_token = None # token was rejected outright; force one refresh continue
if response.status_code == 429: retry_after = int(response.headers.get("Retry-After", 1)) if attempt == max_attempts or time.time() - start + retry_after > max_elapsed: response.raise_for_status() time.sleep(retry_after) continue
return response return response
def poll(self, url, is_done, interval=15, max_elapsed=3600): """GET `url` through this client until `is_done(body)` or `max_elapsed` seconds pass.""" start = time.time() while True: response = self.request("GET", url) response.raise_for_status() body = response.json() if is_done(body): return body if time.time() - start >= max_elapsed: raise TimeoutError(f"Gave up polling {url} after {max_elapsed}s") time.sleep(interval)Use it in place of requests directly, for example the token and organization lookup from Steps 1-2 above:
client = RCAPIClient(client_id="your-client-id", client_secret="your-client-secret")api_info = client.request("GET", "https://cloud-api.prevu3d.com/oauth/api-info").json()request() only backs off once it sees a 429. If a workload runs close to a bucket’s capacity without tripping one (sustained high-volume traffic, for example), read X-RateLimit-Remaining and X-RateLimit-Reset from any response and slow down before the bucket empties — see Rate Limits. poll() builds a backoff-and-give-up polling loop on top of request(); Processing and Monitoring uses it.
What’s next?
Section titled “What’s next?”You’ve gone from a fresh access token to a change you could see in the Prevu3D Platform, and back.
- Finding Your IDs: more ways to resolve organization, division, site, and other node IDs beyond browsing.
- Node Types and Hierarchy: the full node tree you just walked through, and what each node type can contain.
- Introduction: the task-based starting point for everything else the RealityConnect API can do.