HomeGuidesAPI ReferenceChangelogDiscussions
Hire a ReactVision Expert 🛠️Launch Studio 🖥️Sponsor on GitHub ❤️Log In
Guides

Cloud Anchors

Cloud Anchors - Cross-Platform AR Anchor Sharing

Cloud Anchors enable you to share AR experiences across multiple devices and platforms (iOS and Android). When you host an anchor, it's uploaded to a cloud provider and can be resolved by any device with the cloud anchor ID.

🚀

New in v2.53.0 - ReactVision is the default provider

As of v2.53.0, cloud anchors use ReactVision Spatial ("reactvision" provider) by default. You will need a client key as rvApiKey and its project's ID as rvProjectId from ReactVision Editor — see Getting Started with ReactVision Spatial for details. The existing hostCloudAnchor / resolveCloudAnchor API is unchanged regardless of provider. In 3.0.1, onCloudAnchorStateChange was removed (it was never wired) — poll getCloudAnchorStatus() for resolve progress, or read state from the host/resolve result. See VPS.

To use Google Cloud Anchors instead, explicitly set provider="arcore" on your ViroARSceneNavigator and follow the Google Cloud Setup below. The old cloudAnchorProvider prop is deprecated — use provider instead.

📘

Building multi-device shared scenes? Prefer <ViroARCloudAnchor> (3.0.0+) so children sit in the shared location frame automatically. For room-scale scan → host → mesh, see VPS.

📘

cancelCloudAnchorOperations() (3.0.2+)

Now actually cancels pending ReactVision hosts, resolves, and scans (ErrorCancelled). It was previously a no-op. See ViroARSceneNavigator.

Features

  • Cross-Platform: Share anchors between iOS and Android devices — iOS host → Android resolve and vice versa is fully supported
  • Persistent: Anchors can be stored for 1–365 days via the ttlDays parameter
  • Accurate: Sub-centimeter accuracy in good conditions
  • Scalable: Supports multiple concurrent users
  • GPS tagging (ReactVision): Set the device location before hosting to embed GPS coordinates as anchor metadata, enabling proximity search via rvFindNearbyCloudAnchors
  • Immediate error feedback (ReactVision): hostCloudAnchor returns an error immediately if the environment has not been sufficiently observed — no silent fallback

Requirements

Regardless of which provider you choose, the following requirements apply:

  • iOS: iOS 12.0+ with ARKit support, ARCore SDK for iOS
  • Android: Android 7.0+ (API 24) with ARCore support
  • Network: Internet connection required for cloud anchor operations

See the Provider Setup guide for a full comparison of the ReactVision and ARCore providers, and setup instructions for each.


Setup

For full setup instructions for both the ReactVision and ARCore providers, see the Provider Setup guide.

Cloud Anchors require camera and internet permissions. If you are also using GPS tagging (via rvFindNearbyCloudAnchors), you will additionally need location permissions — see the Provider Setup guide for details.

Usage

Basic Example - Hosting

Use ViroARPlaneSelector to let users tap on a detected plane to place and host an anchor:

import React, { useRef, useState } from "react";
import {
  ViroARSceneNavigator,
  ViroARScene,
  ViroARPlaneSelector,
  ViroNode,
  ViroBox,
} from "@reactvision/react-viro";

function HostScene({ sceneNavigator }) {
  const [cloudAnchorId, setCloudAnchorId] = useState<string | null>(null);
  const [isHosting, setIsHosting] = useState(false);

  const handlePlaneSelected = async (anchor) => {
    if (isHosting) return;
    setIsHosting(true);

    try {
      // Host the anchor with TTL of 1 day (API key auth limit)
      const result = await sceneNavigator.hostCloudAnchor(anchor.anchorId, 1);

      if (result.success && result.cloudAnchorId) {
        console.log("Cloud Anchor ID:", result.cloudAnchorId);
        setCloudAnchorId(result.cloudAnchorId);
        // Share this ID with other users via your backend
      } else {
        console.error("Failed to host:", result.error);
      }
    } catch (error) {
      console.error("Failed to host anchor:", error);
    } finally {
      setIsHosting(false);
    }
  };

  return (
    <ViroARScene>
      <ViroARPlaneSelector onPlaneSelected={handlePlaneSelected}>
        {/* This content appears at the anchor location */}
        <ViroBox
          position={[0, 0.05, 0]}
          scale={[0.1, 0.1, 0.1]}
          materials={["boxMaterial"]}
        />
      </ViroARPlaneSelector>
    </ViroARScene>
  );
}

export default function App() {
  return (
    <ViroARSceneNavigator
      // provider defaults to "reactvision" — set provider="arcore" to use Google Cloud Anchors instead
      initialScene={{ scene: HostScene }}
    />
  );
}

Basic Example - Resolving

Resolve a cloud anchor and place content at its position:

function ResolveScene({ sceneNavigator, cloudAnchorIdToResolve }) {
  const [resolvedPosition, setResolvedPosition] = useState(null);

  useEffect(() => {
    const resolve = async () => {
      try {
        const result = await sceneNavigator.resolveCloudAnchor(
          cloudAnchorIdToResolve
        );

        if (result.success && result.anchor) {
          console.log("Resolved at:", result.anchor.position);
          setResolvedPosition(result.anchor.position);
        } else {
          console.error("Failed to resolve:", result.error);
        }
      } catch (error) {
        console.error("Failed to resolve anchor:", error);
      }
    };

    if (cloudAnchorIdToResolve) {
      resolve();
    }
  }, [cloudAnchorIdToResolve]);

  return (
    <ViroARScene>
      {resolvedPosition && (
        <ViroNode position={resolvedPosition}>
          <ViroBox
            position={[0, 0.05, 0]}
            scale={[0.1, 0.1, 0.1]}
            materials={["boxMaterial"]}
          />
        </ViroNode>
      )}
    </ViroARScene>
  );
}

API Reference

ViroARSceneNavigator Props

PropTypeDescription
provider"reactvision" | "arcore" | "none"Selects the cloud anchor backend. Defaults to "reactvision" (ReactVision Spatial — no Google Cloud setup needed). Set to "arcore" to use Google Cloud Anchors (requires API key). Set to "none" to disable.
onCloudAnchorStateChange(event) => voidCallback fired when cloud anchor state changes.

sceneNavigator Methods

hostCloudAnchor(anchorId: string, ttlDays?: number): Promise<ViroHostCloudAnchorResult>

Hosts a local anchor to the cloud.

ParameterTypeDescription
anchorIdstringThe local anchor ID (from ViroAnchor.anchorId)
ttlDaysnumberTime-to-live in days (1-365). Default: 1

Returns:

{
  success: boolean;
  cloudAnchorId?: string;  // The cloud anchor ID to share
  error?: string;
  state: ViroCloudAnchorState;
}
resolveCloudAnchor(cloudAnchorId: string): Promise<ViroResolveCloudAnchorResult>

Resolves a cloud anchor by its ID.

ParameterTypeDescription
cloudAnchorIdstringThe cloud anchor ID to resolve

Returns:

{
  success: boolean;
  anchor?: {
    anchorId: string;
    cloudAnchorId: string;
    position: [number, number, number];
    rotation: [number, number, number];
    scale: [number, number, number];
    state: ViroCloudAnchorState;
  };
  node?: ViroARNode;  // May be null for cloud anchors
  error?: string;
  state: ViroCloudAnchorState;
}

Note: The node property may be null for resolved cloud anchors. Use the anchor.position to place content at the resolved location.

cancelCloudAnchorOperations(): void (fixed in 3.0.2)

Cancels pending ReactVision cloud-anchor hosts, resolves, and an open scan window. Each cancelled operation reports ErrorCancelled. Before 3.0.2 this was a no-op on both platforms. ARCore's own cloud-anchor tasks have no cancel API and still run to completion. Co-location in Editor scenes calls this when leaving mid-scan or mid-host.

ReactVision-Only Methods

The following 8 methods are only available when using the ReactVision provider (the default). They are powered by the proprietary ReactVision Spatial SDK.

MethodDescriptionKey
rvGetCloudAnchor(anchorId)Fetch a single anchor recordClient or secret
rvListCloudAnchors(limit, offset)Paginated list of all project anchorsSecret
rvUpdateCloudAnchor(id, name, desc, isPublic)Rename / re-describe an anchorSecret
rvDeleteCloudAnchor(anchorId)Permanently delete an anchor and its assetsSecret
rvFindNearbyCloudAnchors(lat, lng, radius, limit)GPS proximity searchClient or secret
rvAttachAssetToCloudAnchor(id, url, size, name, type, userId)Attach a hosted fileSecret
rvRemoveAssetFromCloudAnchor(anchorId, assetId)Remove an attached assetSecret
rvTrackCloudAnchorResolution(...)Record resolve analytics manuallyClient or secret

A published app holds a client key, so the methods marked Secret fail in it with "This endpoint does not accept this type of API key". Make those calls from your backend with a secret key (see Secret Keys), or from an internal build that holds one. A legacy key, made before Editor 1.5.0, can still call all eight until ReactVision converts it.

With a client key, rvGetCloudAnchor and rvFindNearbyCloudAnchors never return a private anchor (isPublic: false) and leave out the fields that identify other users, and a nearby search returns at most 50 anchors within 1,000 m.

rvGetCloudAnchor(anchorId)

Fetch a single cloud anchor record.

const result = await sceneNavigator.rvGetCloudAnchor("cloud-anchor-abc");
if (result.success) {
  console.log(result.anchor.name, result.anchor.isPublic);
}
rvListCloudAnchors(limit, offset)

Paginated list of all cloud anchors in your project.

const result = await sceneNavigator.rvListCloudAnchors(20, 0);
result.anchors.forEach(a => console.log(a.anchorId, a.name));
rvUpdateCloudAnchor(id, name, desc, isPublic)

Rename or re-describe an existing cloud anchor.

ParameterTypeDescription
idstringThe cloud anchor ID
namestringNew display name (optional)
descstringNew description (optional)
isPublicbooleanfalse hides the anchor from client keys: only a secret key or an Editor session can reach it (optional)
await sceneNavigator.rvUpdateCloudAnchor(
  "cloud-anchor-abc",
  "Coffee Shop Table",
  "The corner table near the window",
  true
);
rvDeleteCloudAnchor(anchorId)

Permanently delete a cloud anchor and all its attached assets.

await sceneNavigator.rvDeleteCloudAnchor("cloud-anchor-abc");
rvFindNearbyCloudAnchors(lat, lng, radius, limit)

GPS proximity search for cloud anchors. Requires GPS tagging to have been set on the anchors during hosting.

ParameterTypeDescription
latnumberLatitude in degrees
lngnumberLongitude in degrees
radiusnumberSearch radius in meters
limitnumberMaximum number of results
const nearby = await sceneNavigator.rvFindNearbyCloudAnchors(
  37.7749, -122.4194, 200, 10
);
nearby.anchors.forEach(a => console.log(a.anchorId, a.name));
rvAttachAssetToCloudAnchor(id, url, size, name, type, userId)

Attach a hosted file to a cloud anchor so it loads automatically when resolved. Upload the file first with rvUploadAsset (see Assets Storage). Both calls need a secret key, so in a published app, upload and attach from your backend and host the anchor in the app. The example below runs as written only with a secret key, in an internal build.

ParameterTypeDescription
idstringThe cloud anchor ID
urlstringURL of the hosted asset
sizenumberFile size in bytes
namestringDisplay name for the asset
typestringAsset type ("3d-model", "image", etc.)
userIdstringThe app user ID who owns the asset
const uploadResult = await sceneNavigator.rvUploadAsset(
  "3d-model", "decoration.glb", fileData, "user-123"
);

const hostResult = await sceneNavigator.hostCloudAnchor(localAnchorId, 30);

if (uploadResult.success && hostResult.success) {
  await sceneNavigator.rvAttachAssetToCloudAnchor(
    hostResult.cloudAnchorId,
    uploadResult.url,
    fileData.length,
    "decoration.glb",
    "3d-model",
    "user-123"
  );
}
rvRemoveAssetFromCloudAnchor(anchorId, assetId)

Remove a previously attached asset from a cloud anchor.

await sceneNavigator.rvRemoveAssetFromCloudAnchor(
  "cloud-anchor-abc",
  "asset-xyz-789"
);
rvTrackCloudAnchorResolution(...)

Record resolve analytics manually. Useful for tracking how often and where your cloud anchors are being resolved.

await sceneNavigator.rvTrackCloudAnchorResolution(
  cloudAnchorId,
  resolveTimeMs,
  success
);

Cloud Anchor States

type ViroCloudAnchorState =
  | "None"
  | "Success"
  | "ErrorInternal"
  | "TaskInProgress"
  | "ErrorNotAuthorized" // Invalid or missing API key
  | "ErrorResourceExhausted" // Quota exceeded
  | "ErrorHostingDatasetProcessingFailed" // Need more visual data
  | "ErrorCloudIdNotFound" // Cloud anchor expired or doesn't exist
  | "ErrorResolvingSdkVersionTooOld"
  | "ErrorResolvingSdkVersionTooNew"
  | "ErrorHostingServiceUnavailable";

Best Practices

1. Ensure Good Tracking Quality

Cloud anchors work best when:

  • The environment has distinct visual features
  • Lighting is consistent
  • The device has moved around the anchor location
  • At least 30 seconds of tracking data is available
// Wait for good tracking before hosting
const handleAnchorFound = async (anchor: ViroAnchor) => {
  // Give ARKit/ARCore time to build a good map
  await new Promise((resolve) => setTimeout(resolve, 3000));

  const result = await sceneNavigator.hostCloudAnchor(anchor.anchorId, 7);
  // ...
};

2. Handle Errors Gracefully

const result = await sceneNavigator.hostCloudAnchor(anchorId, 7);

switch (result.state) {
  case "Success":
    // Success!
    break;
  case "ErrorNotAuthorized":
    console.error("Check your API key configuration");
    break;
  case "ErrorResourceExhausted":
    console.error("API quota exceeded. Try again later.");
    break;
  case "ErrorHostingDatasetProcessingFailed":
    console.error("Move the device around to capture more environment data");
    break;
  default:
    console.error("Error:", result.error);
}

3. Use Appropriate TTL

Use CaseRecommended TTL
Real-time multiplayer1 day
Event-based experience1-7 days
Persistent installation365 days

Important: API key authentication only supports TTL of 1 day. For TTL > 1 day, you need to configure keyless authorization using OAuth or Service Accounts. See ARCore Cloud Anchors Authorization for details.

4. Share Cloud Anchor IDs

Cloud anchor IDs need to be shared between devices. Common approaches:

  • Firebase Realtime Database: Real-time sync for multiplayer
  • QR Codes: Simple sharing for co-located experiences
  • REST API: Your own backend for custom logic
  • Nearby Connections: Peer-to-peer sharing

Quotas and Limits

ResourceLimit
Host requests30 per minute per project
Resolve requests300 per minute per project
Anchor TTL1-365 days
Cloud anchor size~50KB per anchor

Troubleshooting

"ErrorNotAuthorized"

  • Verify your API key is correct
  • Ensure ARCore API is enabled in Google Cloud Console
  • Check that the API key is properly configured for your bundle ID / package name

"ErrorHostingDatasetProcessingFailed"

  • Move the device around more to capture environment data
  • Ensure good lighting conditions
  • Try hosting in an area with more visual features

"ErrorCloudIdNotFound"

  • The cloud anchor may have expired (exceeded TTL)
  • The cloud anchor ID may be incorrect
  • Network connectivity issues

iOS: "GARSession initialization failed"

  • Verify GARAPIKey is in Info.plist
  • Ensure ARCore/CloudAnchors pod is installed
  • Check Xcode build settings (Bitcode = NO)

Android: Cloud anchors not working

  • Verify API key in AndroidManifest.xml
  • Ensure device supports ARCore
  • Check that ARCore app is installed and updated

Example: Multiplayer AR Game

import { useEffect, useState } from "react";
import { database } from "./firebase"; // Your Firebase setup

function MultiplayerARScene({ sceneNavigator, roomId }) {
  const [sharedObjects, setSharedObjects] = useState([]);

  // Listen for cloud anchors shared by other players
  useEffect(() => {
    const unsubscribe = database
      .ref(`rooms/${roomId}/anchors`)
      .on("child_added", async (snapshot) => {
        const { cloudAnchorId, objectType } = snapshot.val();

        const result = await sceneNavigator.resolveCloudAnchor(cloudAnchorId);
        if (result.success && result.anchor) {
          setSharedObjects((prev) => [
            ...prev,
            {
              id: snapshot.key,
              position: result.anchor.position,
              objectType,
            },
          ]);
        }
      });

    return () => unsubscribe();
  }, [roomId]);

  // Host a new object and share with other players
  const placeObject = async (anchor, objectType) => {
    const result = await sceneNavigator.hostCloudAnchor(anchor.anchorId, 1);

    if (result.success) {
      // Share with other players via Firebase
      await database.ref(`rooms/${roomId}/anchors`).push({
        cloudAnchorId: result.cloudAnchorId,
        objectType,
        timestamp: Date.now(),
      });
    }
  };

  return (
    <ViroARScene onAnchorFound={(anchor) => placeObject(anchor, "cube")}>
      {sharedObjects.map((obj) => (
        <ViroNode key={obj.id} position={obj.position}>
          <ViroBox scale={[0.1, 0.1, 0.1]} />
        </ViroNode>
      ))}
    </ViroARScene>
  );
}