Skip to content

Native Application Flow

Use the Native app flow for integrations that act on behalf of a signed-in user from their own machine, such as a desktop app, CLI, or CAD plugin. It uses Authorization Code with PKCE, a localhost loopback redirect, and no client secret. This guide walks you through setup from OAuth application creation to your first API call.

Review the Getting Started guide first if you have not already covered prerequisites, network access, and the security model.


  1. Create an OAuth application: Register a native app in Prevu3D and get a Client ID
  2. Configure user access: Make sure the signing-in user can reach the data it needs
  3. Authorize with PKCE: Send the user through the browser consent page
  4. Exchange the code for a token: Trade the authorization code for an access token
  5. Find your API URL: Discover your organization’s base URL (it varies by region)
  6. Make your first call: Test the connection with a simple request
  1. Log in to the Prevu3D Platform (or your preproduction environment).
  2. Go to SettingsOAuth.
  3. Click to create a new application.
  4. Enable Authorization Code flow.
  5. Enable Native Application. The redirect URI is fixed to http://localhost.
  6. Leave the client secret off. Native apps do not use one.
  7. Copy and securely store your Client ID. You will need it for every authorization.

The native flow calls the API as the signed-in user, not a service user. This covers layers 2 and 3 of the security model: which nodes the user can see (content access), and what it can do with them (role / permission access).

  1. Make sure the user who will sign in has content access to the nodes (organizations, divisions, sites, etc.) your use case requires.
  2. Make sure the user has a role on those nodes with the permissions your integration needs (read, edit, manage, etc.).

On the consent screen, the user also selects which organization to authorize. The scopes the user approves there configure layer 1.

Native apps cannot keep a secret, so the flow uses PKCE to protect the authorization code. Before opening the consent page, generate three values:

ValueHow
code_verifierRandom string, 43 to 128 characters
code_challengeBASE64URL(SHA256(code_verifier)) without padding
stateRandom string; validate it when the redirect comes back

Start a local loopback server on a free port, then open the user’s browser to the consent page.

Production consent URL: https://cloud.prevu3d.com/oauth

Preproduction consent URL: https://cloud.preproduction.prevu3d-int.com/oauth

Query parameters (all required):

ParameterValue
response_typecode
code_challenge_methodS256
client_idYour Client ID
redirect_uriYour loopback URI, e.g. http://localhost:8765 (include the port, no trailing slash)
scopesOne per scope, e.g. scopes=read:basic&scopes=read:hierarchy
code_challengeThe PKCE challenge from above
stateYour random state value

After the user signs in and clicks Allow, the browser redirects to your loopback URI:

http://localhost:8765/?code={authorization_code}&state={state}

If the user denies access, you receive ?error=access_denied instead. Authorization codes expire after 60 seconds, so exchange them right away.

Step 4: Exchange the code for an access token

Section titled “Step 4: Exchange the code for an access token”

Endpoint: POST https://cloud-api.prevu3d.com/oauth/token

Headers: Content-Type: application/x-www-form-urlencoded

Body:

FieldValue
grant_typeauthorization_code
client_idYour Client ID
codeCode from the redirect
redirect_uriThe same URI used in Step 3 (including the port)
code_verifierThe original PKCE verifier

Do not send a client secret for native apps.

Example response:

{
"hasError": false,
"expires_in": 86400,
"access_token": "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "..."
}

Save both tokens. Use the access_token for API calls. When it expires, request a new one with grant_type=refresh_token and your stored refresh_token (no client secret required).

Call the discovery endpoint to get the apiUrl for your organization. Do not hardcode a regional URL.

Endpoint: GET https://cloud-api.prevu3d.com/oauth/api-info

Headers: Authorization: Bearer <your_access_token>

Example response:

{
"user": { "..." : "..." },
"organization": {
"id": "217ebd23-ec54-4af0-a6d6-4a441a6d1966",
"name": "Test Organization"
},
"scopes": ["read:basic", "read:hierarchy"],
"apiUrl": "https://api-ue1.prevu3d.com/realityconnect-api"
}

Use the apiUrl value as the base for all your API calls. Also note the organization.id, which you will need for many endpoints.

You now have everything you need: an access token and your base URL. Let’s make a simple request to retrieve your organization’s divisions:

GET {apiUrl}/v1/nodes/{organization_id}/browse
Authorization: Bearer <your_access_token>

Example with real values:

GET https://api-ue1.prevu3d.com/realityconnect-api/v1/nodes/217ebd23-ec54-4af0-a6d6-4a441a6d1966/browse HTTP/1.1
Authorization: Bearer eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9...

A successful response containing a list of divisions confirms that your integration is working correctly.

Here’s a complete script that does everything above. Replace client_id with your Client ID, then run it. It starts a temporary loopback server, opens the consent page in your browser, and prints your organization’s divisions once you approve.

import base64
import hashlib
import json
import secrets
import webbrowser
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import parse_qs, quote, urlencode, urlparse
import requests
client_id = "your-client-id"
base_url = "https://cloud-api.prevu3d.com"
scopes = ["read:basic", "read:hierarchy"]
# Step 1: Authorize with PKCE and get an access token
verifier = secrets.token_urlsafe(48)[:64]
challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).decode().rstrip("=")
state = secrets.token_urlsafe(32)
auth_result = {"code": None, "state": None}
class Handler(BaseHTTPRequestHandler):
def log_message(self, *args):
pass
def do_GET(self):
query = parse_qs(urlparse(self.path).query)
auth_result["code"] = query.get("code", [None])[0]
auth_result["state"] = query.get("state", [None])[0]
self.send_response(200)
self.end_headers()
server = HTTPServer(("localhost", 0), Handler)
redirect_uri = f"http://localhost:{server.server_address[1]}"
consent_params = urlencode(
{
"response_type": "code",
"code_challenge_method": "S256",
"client_id": client_id,
"redirect_uri": redirect_uri,
"code_challenge": challenge,
"state": state,
}
)
consent_url = f"{base_url.replace('cloud-api.', 'cloud.', 1)}/oauth?{consent_params}"
consent_url += "&" + "&".join(f"scopes={quote(scope)}" for scope in scopes)
webbrowser.open(consent_url)
server.handle_request()
if auth_result["state"] != state:
raise RuntimeError("Invalid state")
if not auth_result["code"]:
raise RuntimeError("Authorization failed")
token_response = requests.post(
f"{base_url}/oauth/token",
data={
"grant_type": "authorization_code",
"client_id": client_id,
"code": auth_result["code"],
"redirect_uri": redirect_uri,
"code_verifier": verifier,
},
)
token_response.raise_for_status()
access_token = token_response.json()["access_token"]
# Step 2: Get your API URL and org ID
api_info = requests.get(
f"{base_url}/oauth/api-info",
headers={"Authorization": f"Bearer {access_token}"},
).json()
api_url = api_info["apiUrl"]
organization_id = api_info["organization"]["id"]
# Step 3: Browse divisions
divisions = requests.get(
f"{api_url}/v1/nodes/{organization_id}/browse",
headers={"Authorization": f"Bearer {access_token}"},
).json()
print("Divisions:", json.dumps(divisions, indent=2))
If you see…Try this…
Invalid OAuth grant request after clicking AllowConfirm you are using the Client ID of a native app (Authorization Code and Native app enabled). A Client Credentials-only app has no redirect URI and cannot complete this flow.
Redirect URI domain must be verified when creating the appDo not enter a custom redirect URI. Turn on the Native app toggle instead. Only http://localhost (no port, no secret) can be registered today.
Consent page error about query parametersEnsure response_type=code, code_challenge_method=S256, and all required params are present, including state and scopes.
400 or 403 on token exchangeThe code may have expired (60 second limit), the redirect_uri may not match Step 3, or the PKCE verifier and challenge may not match.
403 Forbidden when calling the APIA request must pass all three security layers. Check the OAuth scopes, the signed-in user’s content access, and the user’s role on the target node.
Wrong organization in API responsesThe user selects the organization on the consent screen. Re-authorize and pick the correct one.

For connection, DNS, and API URL issues, see Getting Started.

  • For production use, add logic to refresh your token before it expires using grant_type=refresh_token with your stored refresh token.
  • Explore the API reference to see all available operations.