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.
What you’ll do
Section titled “What you’ll do”- Create an OAuth application: Register a native app in Prevu3D and get a Client ID
- Configure user access: Make sure the signing-in user can reach the data it needs
- Authorize with PKCE: Send the user through the browser consent page
- Exchange the code for a token: Trade the authorization code for an access token
- Find your API URL: Discover your organization’s base URL (it varies by region)
- Make your first call: Test the connection with a simple request
Step 1: Create your OAuth application
Section titled “Step 1: Create your OAuth application”- Log in to the Prevu3D Platform (or your preproduction environment).
- Go to Settings → OAuth.
- Click to create a new application.
- Enable Authorization Code flow.
- Enable Native Application. The redirect URI is fixed to
http://localhost. - Leave the client secret off. Native apps do not use one.
- Copy and securely store your Client ID. You will need it for every authorization.
Step 2: Configure user access
Section titled “Step 2: Configure user access”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).
- Make sure the user who will sign in has content access to the nodes (organizations, divisions, sites, etc.) your use case requires.
- 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.
Step 3: Authorize with PKCE
Section titled “Step 3: Authorize with PKCE”Native apps cannot keep a secret, so the flow uses PKCE to protect the authorization code. Before opening the consent page, generate three values:
| Value | How |
|---|---|
code_verifier | Random string, 43 to 128 characters |
code_challenge | BASE64URL(SHA256(code_verifier)) without padding |
state | Random 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):
| Parameter | Value |
|---|---|
response_type | code |
code_challenge_method | S256 |
client_id | Your Client ID |
redirect_uri | Your loopback URI, e.g. http://localhost:8765 (include the port, no trailing slash) |
scopes | One per scope, e.g. scopes=read:basic&scopes=read:hierarchy |
code_challenge | The PKCE challenge from above |
state | Your 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:
| Field | Value |
|---|---|
grant_type | authorization_code |
client_id | Your Client ID |
code | Code from the redirect |
redirect_uri | The same URI used in Step 3 (including the port) |
code_verifier | The 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).
Step 5: Get your API base URL
Section titled “Step 5: Get your API base URL”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.
Step 6: Make your first API call
Section titled “Step 6: Make your first API call”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}/browseAuthorization: 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.1Authorization: Bearer eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9...A successful response containing a list of divisions confirms that your integration is working correctly.
Try it with Python
Section titled “Try it with Python”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 base64import hashlibimport jsonimport secretsimport webbrowserfrom http.server import BaseHTTPRequestHandler, HTTPServerfrom 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 tokenverifier = 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 IDapi_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 divisionsdivisions = requests.get( f"{api_url}/v1/nodes/{organization_id}/browse", headers={"Authorization": f"Bearer {access_token}"},).json()
print("Divisions:", json.dumps(divisions, indent=2))Troubleshooting
Section titled “Troubleshooting”| If you see… | Try this… |
|---|---|
| Invalid OAuth grant request after clicking Allow | Confirm 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 app | Do 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 parameters | Ensure response_type=code, code_challenge_method=S256, and all required params are present, including state and scopes. |
| 400 or 403 on token exchange | The 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 API | A 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 responses | The 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.
What’s next?
Section titled “What’s next?”- For production use, add logic to refresh your token before it expires using
grant_type=refresh_tokenwith your stored refresh token. - Explore the API reference to see all available operations.