expo-beacon
An Expo module for scanning, pairing, and monitoring iBeacons and Eddystone beacons in React Native apps — with full background support on both iOS and Android.
| Feature | Description |
|---|---|
| Scan | Discover nearby iBeacons (one-shot or continuous) and Eddystone-UID / Eddystone-URL beacons via BLE |
| Pair | Register specific beacons for persistent tracking — survives app restarts |
| Monitor | Background enter/exit region detection with distance-based filtering |
| Distance | Real-time distance updates (~1/sec) while monitoring |
| Timeout | Fire a one-shot event after a beacon has been out of range for a configured duration |
| Event Logging | Persist every beacon event to a local SQLite database for diagnostics & replay |
| Notifications | Automatic local notifications on region enter/exit, fully customisable |
| Platform | Native Implementation |
|---|---|
| Android | AltBeacon library + Foreground Service |
| iOS | CoreLocation (iBeacon ranging & monitoring) + CoreBluetooth (Eddystone & wildcard BLE) |
| Web | Not supported (async methods reject, sync getters return inert defaults, everything else throws) |
Table of Contents
- Installation
- Platform Setup
- Quick Start
- React Hooks
- Usage Examples
- Full API Reference
- requestPermissionsAsync()
- scanForBeaconsAsync()
- scanForEddystonesAsync()
- startContinuousScan()
- stopContinuousScan()
- cancelScan()
- pairBeacon()
- unpairBeacon()
- getPairedBeacons()
- pairEddystone()
- unpairEddystone()
- getPairedEddystones()
- startMonitoring()
- stopMonitoring()
- getMonitoringConfig()
- getMonitoredDeviceState()
- getMonitoredDeviceStates()
- setNotificationConfig()
- enableEventLogging()
- disableEventLogging()
- getEventLogs()
- clearEventLogs()
- destroyEventLogs()
- setApiEndpoint()
- Events
- TypeScript Types
- Native Integrations
- Background Behaviour
- Notifications
- Platform-Specific Notes & Gotchas
- Troubleshooting
- Error Codes
- Contributing
- License
Installation
npx expo install expo-beaconImportant: This module contains native code and cannot be used with Expo Go. You must use a development build or a bare workflow.
Platform Setup
iOS
1. Info.plist Keys
Add the following keys to your Info.plist (or use an Expo config plugin):
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This app monitors iBeacons in the background.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app uses location to detect nearby beacons.</string>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>This app uses Bluetooth to scan for iBeacons.</string>2. Background Modes
In Xcode under Signing & Capabilities, enable:
- Background Modes → Location updates
- Background Modes → Uses Bluetooth LE accessories
When the bundled config plugin is installed (
"plugins": ["expo-beacon"]),locationis merged intoUIBackgroundModesautomatically onexpo prebuild— the native module only enables background ranging when this mode is present.
Key iOS Constraints
- 20 monitored regions max: iOS limits
CLLocationManagerto 20 simultaneously monitored beacon regions. If you pair more than 20 iBeacons, only the first 20 are monitored. Eddystone beacons use BLE scanning and do not count toward this limit. - No wildcard iBeacon scanning: Apple strips iBeacon manufacturer data from CoreBluetooth advertisements. You must supply at least one proximity UUID when scanning, or have paired beacons (the module auto-uses their UUIDs).
- Eddystone works unrestricted: Eddystone uses standard BLE service data (
0xFEAA), which iOS does not strip. BothscanForEddystonesAsync()and continuous scanning discover Eddystones without restrictions.
Android
All required permissions are declared in the module's AndroidManifest.xml and merged automatically. You must still request runtime permissions before scanning or monitoring:
const granted = await ExpoBeacon.requestPermissionsAsync();The module requests: BLUETOOTH_SCAN, BLUETOOTH_CONNECT, ACCESS_FINE_LOCATION, and POST_NOTIFICATIONS (API 33+).
Quick Start
A minimal example that pairs one iBeacon and one Eddystone, starts monitoring, and scans for nearby beacons:
import { useEffect, useState } from "react";
import { Button, FlatList, Text, View } from "react-native";
import ExpoBeacon from "expo-beacon";
import type { BeaconScanResult, BeaconRegionEvent } from "expo-beacon";
export default function App() {
const [beacons, setBeacons] = useState<BeaconScanResult[]>([]);
useEffect(() => {
// 1. Pair beacons you want to monitor
ExpoBeacon.pairBeacon(
"lobby-entrance",
"E2C56DB5-DFFB-48D2-B060-D0F5A71096E0",
1,
100,
);
// 2. Listen for enter/exit events
const enterSub = ExpoBeacon.addListener("onBeaconEnter", (e: BeaconRegionEvent) => {
console.log(`Entered ${e.identifier} at ${e.distance.toFixed(1)} m`);
});
const exitSub = ExpoBeacon.addListener("onBeaconExit", (e: BeaconRegionEvent) => {
console.log(`Exited ${e.identifier}`);
});
// 3. Request permissions and start monitoring
ExpoBeacon.requestPermissionsAsync().then((granted) => {
if (granted) ExpoBeacon.startMonitoring(10); // enter within 10 m
});
return () => {
enterSub.remove();
exitSub.remove();
ExpoBeacon.stopMonitoring();
};
}, []);
async function scan() {
const results = await ExpoBeacon.scanForBeaconsAsync(
["E2C56DB5-DFFB-48D2-B060-D0F5A71096E0"],
5000
);
setBeacons(results);
}
return (
<View style={{ flex: 1, padding: 20, paddingTop: 60 }}>
<Button title="Scan 5 s" onPress={scan} />
<FlatList
data={beacons}
keyExtractor={(b) => `${b.uuid}-${b.major}-${b.minor}`}
renderItem={({ item: b }) => (
<Text>{b.uuid} {b.major}/{b.minor} — {b.distance.toFixed(1)} m</Text>
)}
/>
</View>
);
}React Hooks
For React / React Native apps the package ships two hooks that wrap the imperative API, manage event subscriptions (with automatic cleanup), and expose the relevant state reactively. Import them directly from the package:
import { useBeacon, useCarPlay } from "expo-beacon";Both hooks accept optional event callbacks. Callbacks are read from a ref, so passing fresh inline functions on every render does not re-subscribe the underlying native listeners.
useBeacon()
Manages scanning and background monitoring. It keeps the paired-beacon lists, the set of beacons currently in range, and the monitoring flag in sync, and returns stable action wrappers.
import { useBeacon } from "expo-beacon";
function BeaconScreen() {
const {
inRange,
isMonitoring,
pairedBeacons,
requestPermissions,
pairBeacon,
startMonitoring,
stopMonitoring,
} = useBeacon({
onBeaconEnter: (e) => console.log("entered", e.identifier, e.distance),
onBeaconExit: (e) => console.log("exited", e.identifier),
onError: (e) => console.warn(`[${e.code}] ${e.message}`),
});
return (
<View>
<Button title="Grant permissions" onPress={requestPermissions} />
<Button
title={isMonitoring ? "Stop monitoring" : "Start monitoring"}
onPress={() => (isMonitoring ? stopMonitoring() : startMonitoring())}
/>
{inRange.map((b) => (
<Text key={b.identifier}>
{b.identifier} — {b.distance >= 0 ? `${b.distance.toFixed(1)}m` : "n/a"}
</Text>
))}
</View>
);
}| Returned value | Description |
|---|---|
pairedBeacons / pairedEddystones |
Reactive lists of paired devices. |
inRange |
Paired beacons currently in range, derived live from enter/exit/distance/timeout events (InRangeBeacon[]). |
isMonitoring |
Whether background monitoring is active. |
isEventLoggingEnabled |
Whether SQLite event logging is enabled (kept in sync by the logging actions). |
refreshPaired() |
Re-read the paired lists from native. |
pairBeacon() / unpairBeacon() |
Pair / unpair an iBeacon, then refresh. |
pairEddystone() / unpairEddystone() |
Pair / unpair an Eddystone, then refresh. |
scanForBeacons() / scanForEddystones() |
One-shot scans returning a promise. |
startContinuousScan() / stopContinuousScan() |
Live scan; results arrive via onBeaconFound / onEddystoneFound. |
cancelScan() |
Cancel an in-progress one-shot scan. |
startMonitoring() / stopMonitoring() |
Start / stop background monitoring. |
getMonitoringConfig() |
Read the current monitoring config + active-state snapshot. |
getMonitoredDeviceState() / getMonitoredDeviceStates() |
Native state snapshot for one / all paired devices. |
setNotificationConfig() |
Persist notification configuration for monitoring sessions. |
setBeaconNotificationConfig() |
Persist only beacon notification settings. |
setCarPlayNotificationConfig() |
Persist only CarPlay / Android Auto notification settings. |
enableEventLogging() / disableEventLogging() |
Toggle SQLite logging (updates isEventLoggingEnabled). |
getEventLogs() / clearEventLogs() / destroyEventLogs() |
Read / clear / drop the persisted event log. |
setApiEndpoint() / getApiEndpoint() |
Configure / read the native event-forwarding endpoint. |
isBatteryOptimizationExempt() / requestBatteryOptimizationExemption() |
Check / request Android battery-optimization exemption. |
requestPermissions() |
Request the permissions needed for scanning / monitoring. |
inRange reflects monitored (paired) beacons only. Continuous-scan results
are delivered through the onBeaconFound / onEddystoneFound callbacks because
raw scan hits carry no paired identifier. Pass track: false to skip inRange
bookkeeping when you only need the callbacks.
useCarPlay()
Observes CarPlay / Android Auto connection state. It initializes from the persisted native state on mount and tracks live connect / disconnect events.
import { useCarPlay } from "expo-beacon";
function CarPlayBadge() {
const { connected, transport, isMonitoring, startMonitoring, stopMonitoring } =
useCarPlay({
onConnected: (e) => console.log("car connected via", e.transport),
onDisconnected: () => console.log("car disconnected"),
});
return (
<View>
<Text>{connected ? `Connected (${transport})` : "Not connected"}</Text>
<Button
title={isMonitoring ? "Stop" : "Start"}
onPress={() => (isMonitoring ? stopMonitoring() : startMonitoring())}
/>
</View>
);
}| Returned value | Description |
|---|---|
connected |
Whether a CarPlay / Android Auto session is active. |
transport |
Transport of the active session (CarPlayTransport), or null. |
isMonitoring |
Whether persistent monitoring is enabled. |
lastConnectedAt / lastDisconnectedAt |
Epoch-ms timestamps of the last transitions, or null. |
startMonitoring() / stopMonitoring() |
Enable / disable monitoring. |
refresh() |
Re-read the connection + monitoring state from native. |
getDiagnostics() |
Fetch detection diagnostics for troubleshooting. |
Pass autoStart: true to call startCarPlayMonitoring() on mount when it is
not already enabled.
Both hooks are safe to call on web: the underlying module is a no-op stub there, so the hooks simply report empty / disconnected state.
Usage Examples
Scanning for iBeacons
One-shot scan with UUID filter (both platforms)
import ExpoBeacon from "expo-beacon";
// Scan for 8 seconds, filtering by a specific UUID
const beacons = await ExpoBeacon.scanForBeaconsAsync(
["E2C56DB5-DFFB-48D2-B060-D0F5A71096E0"],
8000,
);
beacons.forEach((b) => {
console.log(
`UUID: ${b.uuid} Major: ${b.major} Minor: ${b.minor} ` +
`Distance: ${b.distance.toFixed(1)}m RSSI: ${b.rssi}dBm`
);
});Wildcard scan (Android only)
// Pass an empty array (or omit the arguments — defaults are uuids = [],
// scanDuration = 5000) to discover ALL nearby iBeacons.
// On iOS, this auto-uses UUIDs from paired beacons
const beacons = await ExpoBeacon.scanForBeaconsAsync([], 5000);Multiple UUID scan
// Scan for beacons from two different manufacturers/deployments
const beacons = await ExpoBeacon.scanForBeaconsAsync(
[
"E2C56DB5-DFFB-48D2-B060-D0F5A71096E0",
"FDA50693-A4E2-4FB1-AFCF-C6EB07647825",
],
10000,
);Scanning for Eddystone Beacons
import ExpoBeacon from "expo-beacon";
// Discover both Eddystone-UID and Eddystone-URL frames
const eddystones = await ExpoBeacon.scanForEddystonesAsync(5000);
eddystones.forEach((b) => {
if (b.frameType === "uid") {
console.log(`UID: namespace=${b.namespace} instance=${b.instance} dist=${b.distance.toFixed(1)}m`);
} else if (b.frameType === "url") {
console.log(`URL: ${b.url} dist=${b.distance.toFixed(1)}m`);
}
});Eddystone scanning works identically on both iOS and Android — no UUID filter required.
Continuous (Live) Scanning
Use continuous scanning when you need real-time beacon updates (e.g., a live radar UI). This fires events continuously rather than resolving a single promise.
import { useEffect, useRef, useState } from "react";
import { FlatList, Text, Button, View } from "react-native";
import ExpoBeacon from "expo-beacon";
import type { BeaconScanResult, EddystoneScanResult } from "expo-beacon";
export default function LiveScanner() {
const [ibeacons, setIbeacons] = useState<BeaconScanResult[]>([]);
const [eddystones, setEddystones] = useState<EddystoneScanResult[]>([]);
const [scanning, setScanning] = useState(false);
const subs = useRef<Array<{ remove: () => void }>>([]);
const startScan = () => {
setScanning(true);
// iBeacon advertisements
subs.current.push(
ExpoBeacon.addListener("onBeaconFound", (beacon) => {
setIbeacons((prev) => {
const key = `${beacon.uuid}-${beacon.major}-${beacon.minor}`;
const idx = prev.findIndex(
(b) => `${b.uuid}-${b.major}-${b.minor}` === key,
);
if (idx >= 0) {
const copy = [...prev];
copy[idx] = beacon; // Update distance/RSSI
return copy;
}
return [...prev, beacon];
});
}),
);
// Eddystone advertisements
subs.current.push(
ExpoBeacon.addListener("onEddystoneFound", (beacon) => {
setEddystones((prev) => {
const key = beacon.frameType === "uid"
? `${beacon.namespace}-${beacon.instance}`
: `url-${beacon.url}`;
const idx = prev.findIndex((b) => {
const k = b.frameType === "uid"
? `${b.namespace}-${b.instance}`
: `url-${b.url}`;
return k === key;
});
if (idx >= 0) {
const copy = [...prev];
copy[idx] = beacon;
return copy;
}
return [...prev, beacon];
});
}),
);
ExpoBeacon.startContinuousScan();
};
const stopScan = () => {
ExpoBeacon.stopContinuousScan();
subs.current.forEach((s) => s.remove());
subs.current = [];
setScanning(false);
};
useEffect(() => {
return () => stopScan(); // Cleanup on unmount
}, []);
return (
<View style={{ flex: 1, padding: 20 }}>
<Button
title={scanning ? "Stop Scan" : "Start Live Scan"}
onPress={scanning ? stopScan : startScan}
/>
<Text style={{ fontWeight: "bold", marginTop: 10 }}>
iBeacons ({ibeacons.length})
</Text>
<FlatList
data={ibeacons}
keyExtractor={(b) => `${b.uuid}-${b.major}-${b.minor}`}
renderItem={({ item: b }) => (
<Text>
{b.uuid.slice(0, 8)}… {b.major}/{b.minor} — {b.distance.toFixed(1)}m (RSSI: {b.rssi})
</Text>
)}
/>
<Text style={{ fontWeight: "bold", marginTop: 10 }}>
Eddystones ({eddystones.length})
</Text>
<FlatList
data={eddystones}
keyExtractor={(b, i) => `eddy-${i}`}
renderItem={({ item: b }) => (
<Text>
{b.frameType === "uid"
? `UID: ${b.namespace?.slice(0, 8)}… / ${b.instance}`
: `URL: ${b.url}`} — {b.distance.toFixed(1)}m
</Text>
)}
/>
</View>
);
}iOS note: Continuous iBeacon scanning on iOS only discovers beacons whose UUID has been registered via
pairBeacon(). On Android, all nearby BLE beacons are reported. Eddystone discovery works on both platforms regardless of pairing.
Pairing & Unpairing Beacons
Pairing registers a beacon for persistent monitoring. Paired beacons survive app restarts — they are stored in UserDefaults (iOS) / SharedPreferences (Android).
import ExpoBeacon from "expo-beacon";
// ── iBeacon ──
// Pair an iBeacon (identifier must be unique)
ExpoBeacon.pairBeacon(
"lobby-entrance", // your label
"E2C56DB5-DFFB-48D2-B060-D0F5A71096E0", // proximity UUID
1, // major (0–65535)
100, // minor (0–65535)
);
// Re-pairing with the same identifier replaces the previous entry
ExpoBeacon.pairBeacon(
"lobby-entrance",
"E2C56DB5-DFFB-48D2-B060-D0F5A71096E0",
1,
200, // updated minor
);
// List all paired iBeacons
const paired = ExpoBeacon.getPairedBeacons();
console.log(paired);
// → [{ identifier: "lobby-entrance", uuid: "E2C5…", major: 1, minor: 200 }]
// Remove a beacon
ExpoBeacon.unpairBeacon("lobby-entrance");
// ── Eddystone-UID ──
// Pair an Eddystone-UID beacon
ExpoBeacon.pairEddystone(
"meeting-room", // your label
"edd1ebeac04e5defa017", // 10-byte namespace (20 hex chars)
"0123456789ab", // 6-byte instance (12 hex chars)
);
// List all paired Eddystones
const pairedEddy = ExpoBeacon.getPairedEddystones();
console.log(pairedEddy);
// → [{ identifier: "meeting-room", namespace: "edd1…", instance: "0123…" }]
// Remove an Eddystone
ExpoBeacon.unpairEddystone("meeting-room");Background Monitoring
Monitoring watches all paired beacons (iBeacon + Eddystone) in the background and fires events when the device enters or exits a beacon region.
import { useEffect, useRef } from "react";
import ExpoBeacon from "expo-beacon";
import type {
BeaconRegionEvent,
BeaconDistanceEvent,
EddystoneRegionEvent,
EddystoneDistanceEvent,
} from "expo-beacon";
export function useBeaconMonitoring() {
const subs = useRef<Array<{ remove: () => void }>>([]);
useEffect(() => {
async function start() {
const granted = await ExpoBeacon.requestPermissionsAsync();
if (!granted) {
console.warn("Beacon permissions denied");
return;
}
// Subscribe to iBeacon events
subs.current.push(
ExpoBeacon.addListener("onBeaconEnter", (e: BeaconRegionEvent) => {
console.log(`[iBeacon] Entered "${e.identifier}" at ~${e.distance.toFixed(1)}m`);
}),
ExpoBeacon.addListener("onBeaconExit", (e: BeaconRegionEvent) => {
console.log(`[iBeacon] Exited "${e.identifier}"`);
}),
ExpoBeacon.addListener("onBeaconDistance", (e: BeaconDistanceEvent) => {
console.log(`[iBeacon] "${e.identifier}" → ${e.distance.toFixed(2)}m`);
}),
);
// Subscribe to Eddystone events
subs.current.push(
ExpoBeacon.addListener("onEddystoneEnter", (e: EddystoneRegionEvent) => {
console.log(`[Eddystone] Entered "${e.identifier}"`);
}),
ExpoBeacon.addListener("onEddystoneExit", (e: EddystoneRegionEvent) => {
console.log(`[Eddystone] Exited "${e.identifier}"`);
}),
ExpoBeacon.addListener("onEddystoneDistance", (e: EddystoneDistanceEvent) => {
console.log(`[Eddystone] "${e.identifier}" → ${e.distance.toFixed(2)}m`);
}),
);
// Start with distance threshold
await ExpoBeacon.startMonitoring({
maxDistance: 10, // Only fire "enter" within 10 metres
notifications: {
beaconEvents: {
enterTitle: "You're near a beacon!",
exitTitle: "Beacon out of range",
body: "{identifier} {event}ed",
},
},
});
}
start();
return () => {
subs.current.forEach((s) => s.remove());
subs.current = [];
ExpoBeacon.stopMonitoring();
};
}, []);
}Simple shorthand (number = maxDistance)
// Equivalent to { maxDistance: 5 }
await ExpoBeacon.startMonitoring(5);Monitor with no distance filter
// Monitor without distance limit — enter fires as soon as the region is detected
await ExpoBeacon.startMonitoring();Customizing Notifications
Persistent configuration (survives app restarts)
ExpoBeacon.setNotificationConfig({
beacons: {
// Enter/exit/timeout alert notifications (both platforms)
events: {
enabled: true, // Set false to suppress beacon alerts
enterTitle: "Beacon nearby",
exitTitle: "Beacon out of range",
timeoutTitle: "Beacon timed out",
body: "{identifier} {event}ed", // Placeholders: {identifier}, {event}
sound: true, // iOS only
icon: "ic_beacon_notification", // Android only — drawable resource name
},
// Persistent status-bar notification for beacon monitoring (Android only)
foregroundService: {
title: "My App — Beacon monitoring",
text: "Watching for nearby beacons",
icon: "ic_service",
},
// Android notification channel for beacon notifications
channel: {
name: "Proximity Alerts",
description: "Alerts when beacons enter or leave range",
importance: "default", // "low" | "default" | "high"
},
},
carPlay: {
// Connect/disconnect notifications (both platforms)
events: {
enabled: true, // Set false to suppress CarPlay alerts
connectedTitle: "CarPlay connected",
disconnectedTitle: "CarPlay disconnected",
body: "CarPlay {event} {transport}", // Placeholders: {event}, {transport}
sound: true, // iOS only
icon: "ic_carplay_notification", // Android only — drawable resource name
},
// Persistent status-bar notification in CarPlay-only mode (Android only)
foregroundService: {
title: "My App — Vehicle monitoring",
text: "Monitoring CarPlay / Android Auto",
icon: "ic_service",
},
// Android notification channel for CarPlay notifications
channel: {
name: "CarPlay Alerts",
description: "CarPlay and Android Auto connection notifications",
importance: "default",
},
},
});One-off session configuration (inline with startMonitoring)
await ExpoBeacon.startMonitoring({
maxDistance: 5,
notifications: {
beaconEvents: { enabled: false }, // Silent monitoring — no user-facing alerts
},
});Beacon Timeout
Pair a beacon with timeoutSeconds to fire a one-shot event after the beacon has been out of range for that duration. The countdown is armed when the beacon exits range (or when no BLE readings arrive for 60 seconds, e.g. due to Doze mode or background throttling) and is cancelled if the beacon is seen again before it fires.
import { useEffect } from "react";
import ExpoBeacon from "expo-beacon";
import type { BeaconTimeoutEvent, EddystoneTimeoutEvent } from "expo-beacon";
// Pair with a 30-second timeout
ExpoBeacon.pairBeacon(
"lobby-entrance",
"E2C56DB5-DFFB-48D2-B060-D0F5A71096E0",
1,
100,
undefined, // name (optional)
30, // timeoutSeconds — fires 30 s after the beacon leaves range
);
// Pair Eddystone with a 60-second timeout
ExpoBeacon.pairEddystone(
"meeting-room",
"edd1ebeac04e5defa017",
"0123456789ab",
undefined, // name (optional)
60, // timeoutSeconds — fires 60 s after the beacon leaves range
);
// Listen for the timeout events
useEffect(() => {
const beaconTimeout = ExpoBeacon.addListener(
"onBeaconTimeout",
(e: BeaconTimeoutEvent) => {
console.log(`Beacon "${e.identifier}" out of range for configured duration!`);
},
);
const eddystoneTimeout = ExpoBeacon.addListener(
"onEddystoneTimeout",
(e: EddystoneTimeoutEvent) => {
console.log(`Eddystone "${e.identifier}" out of range for configured duration!`);
},
);
return () => {
beaconTimeout.remove();
eddystoneTimeout.remove();
};
}, []);Note: The timeout fires once per exit. If the beacon re-enters range before the countdown completes, the pending timer is cancelled and re-armed on the next exit.
Event Logging
Enable SQLite-backed event logging to persist every beacon event locally. Useful for diagnostics, debugging, and replaying event history.
import ExpoBeacon from "expo-beacon";
import type { EventLogEntry, EventLogQueryOptions } from "expo-beacon";
// Enable logging — creates/opens the SQLite database
ExpoBeacon.enableEventLogging();
// ... scanning, monitoring, etc. — all events are now persisted automatically ...
// Query all recent events
const logs: EventLogEntry[] = ExpoBeacon.getEventLogs();
console.log(logs);
// [
// { id: 42, timestamp: 1712345678000, eventType: "onBeaconEnter",
// identifier: "lobby", data: { uuid: "E2C5…", major: 1, minor: 100, ... } },
// ...
// ]
// Filter by event type and time range
const enterLogs = ExpoBeacon.getEventLogs({
eventType: "onBeaconEnter",
sinceTimestamp: Date.now() - 3600_000, // last hour
limit: 100,
});
// Disable logging (retains existing data)
ExpoBeacon.disableEventLogging();
// Clear all logged events (keeps the database)
ExpoBeacon.clearEventLogs();
// Destroy the database entirely (also disables logging)
ExpoBeacon.destroyEventLogs();Storage: Events are stored in a local SQLite database (
expo_beacon_events.db). No external dependencies are required — Android uses the built-in SQLite, iOS uses the systemlibsqlite3.
Cancelling a Scan
Cancel any in-progress one-shot scan (iBeacon or Eddystone). The pending promise will reject with error code SCAN_CANCELLED.
// Start a long scan
const scanPromise = ExpoBeacon.scanForBeaconsAsync(
["E2C56DB5-DFFB-48D2-B060-D0F5A71096E0"],
30000,
);
// Cancel it after 2 seconds
setTimeout(() => ExpoBeacon.cancelScan(), 2000);
try {
const results = await scanPromise;
} catch (e) {
if (e.code === "SCAN_CANCELLED") {
console.log("Scan was cancelled by user");
}
}CarPlay / Android Auto Detection
Detect when the device connects to a car infotainment system and react in JS — or, when the bundled config plugin is installed, automatically start react-native-background-geolocation tracking on connect and stop it on disconnect.
Detection covers both wired and wireless CarPlay on iOS and Android Auto projection / Android Automotive OS on Android. No special CarPlay entitlement or Android Auto certification is required.
import ExpoBeacon, {
CarPlayConnectedEvent,
CarPlayDisconnectedEvent,
} from "expo-beacon";
// Start observing
await ExpoBeacon.startCarPlayMonitoring();
const connectSub = ExpoBeacon.addListener(
"onCarPlayConnected",
(event: CarPlayConnectedEvent) => {
// event.transport: "wired" | "wireless" | "projection" | "native" | "unknown"
console.log(`Car connected via ${event.transport}`);
},
);
const disconnectSub = ExpoBeacon.addListener(
"onCarPlayDisconnected",
(_event: CarPlayDisconnectedEvent) => {
console.log("Car disconnected");
},
);
// Stop later (e.g. when feature is disabled)
await ExpoBeacon.stopCarPlayMonitoring();
connectSub.remove();
disconnectSub.remove();How it works
- iOS: observes
AVAudioSession.routeChangeNotificationfor output ports of type.carAudio. Wired-vs-wireless is reported on a best-effort basis (looking for a coexisting Bluetooth output port). - Android: observes
androidx.car.app.connection.CarConnectionLiveData.transportis"projection"for phones casting to a head unit,"native"for Android Automotive OS. - Connect/disconnect events flow through the same SQLite event log and remote API forwarder as beacon events.
- When the config plugin is installed, the auto-generated
BeaconGeoPluginalso callsBackgroundGeolocation.start()on connect and.stop()on disconnect — no extra wiring required.
Background detection
CarPlay observation is persistent — the enabled flag is stored in native preferences and the observer is automatically re-attached after app kill or device reboot. startMonitoring() also enables CarPlay observation by default; calling startCarPlayMonitoring() explicitly is only required if you want CarPlay events without beacon monitoring.
- Android: the foreground service hosts the
CarConnectionobserver. As long as the service runs (which it does whenever beacon monitoring or CarPlay monitoring is enabled, and is restarted on boot byBootReceiver), CarPlay events are captured even after the app process is killed. Guaranteed background detection. - iOS: the observer auto-restarts in the module's
OnCreate, including background-launches triggered by beacon region monitoring. iOS cannot wake a terminated app on CarPlay alone — for guaranteed wake-from-suspension, also callstartMonitoring()with at least one paired beacon (e.g. a beacon left in the vehicle). Region-wake events trigger a CarPlay state resync to reconcile any route changes that happened while the app was suspended.
Notes
startCarPlayMonitoring()is idempotent. Calling it twice does not register a duplicate observer.stopCarPlayMonitoring()clears the persisted flag, so the observer will not auto-restart on next launch.- The iOS detector does not require the CarPlay entitlement because it only reads the active audio route; you do not need to ship a CarPlay app.
- On iOS, if the JS bundle is suspended in the background, the JS event delivery is deferred until the app resumes, but the native lifecycle delegate (used by the geolocation plugin) fires immediately on connect.
- On Android, when CarPlay monitoring is enabled without beacon monitoring, the foreground service shows a generic "Connected device monitoring active" notification.
Android Auto registration (automatic via config plugin)
On Android 14+ — and especially with wireless Android Auto — CarConnection.getType() silently returns NOT_CONNECTED for any app that has not declared itself Android-Auto-aware. The bundled config plugin handles this for you: on the next expo prebuild it injects a com.google.android.gms.car.application meta-data entry into AndroidManifest.xml and writes res/xml/automotive_app_desc.xml with <uses name="template"/>. No manual native edits are required.
If you need to disable this (e.g. you already ship your own Android Auto template app and don't want a duplicate registration) or you need to declare a different capability than template, configure the plugin in your app config:
{
"expo": {
"plugins": [
["expo-beacon", {
"android": {
"androidAuto": {
"register": true,
"usesName": "template"
}
}
}]
]
}
}usesName accepts any value supported by Android's automotiveApp schema (template, media, notification, …). Setting register: false skips both the meta-data and the resource file.
Diagnostics
If onCarPlayConnected never fires on Android, call getCarPlayDiagnostics() to inspect the native state:
const d = ExpoBeacon.getCarPlayDiagnostics();
// {
// isCarAppMetadataPresent: true, // false → config plugin didn't run; prebuild again
// isCarProviderQueryable: true, // false → Android Auto app not installed on device
// lastRawConnectionType: 2, // 0=NOT_CONNECTED 1=NATIVE (AAOS) 2=PROJECTION; null = no value yet
// observerActive: true,
// serviceAlive: true,
// }A response with isCarAppMetadataPresent: false indicates the AA registration didn't make it into the built APK — re-run expo prebuild --clean to apply the plugin changes.
Full API Reference
requestPermissionsAsync()
requestPermissionsAsync(): Promise<boolean>Requests all permissions required for scanning and monitoring.
| Platform | Permissions Requested |
|---|---|
| Android | ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION, BLUETOOTH_SCAN + BLUETOOTH_CONNECT (API 31+), POST_NOTIFICATIONS (API 33+), then ACCESS_BACKGROUND_LOCATION (API 29+) in a second prompt. Resolves true only when background location is granted. |
| iOS | CLLocationManager "When In Use" authorization — resolves true once granted. The "Always" upgrade is requested later by startMonitoring(), and Bluetooth permission is not prompted here. |
Returns: true if all required permissions were granted.
const granted = await ExpoBeacon.requestPermissionsAsync();
if (!granted) {
console.warn("Permissions not granted — scanning and monitoring will fail.");
}Tip: Call this before
scanForBeaconsAsync()orstartMonitoring(). If you callstartMonitoring()without prior authorization, it requests "Always" permission automatically, but explicit control gives a better UX.
scanForBeaconsAsync(uuids?, scanDurationMs?)
scanForBeaconsAsync(uuids?: string[], scanDurationMs?: number): Promise<BeaconScanResult[]>Performs a one-shot iBeacon scan. Waits for the specified duration, then resolves with all discovered beacons.
Both parameters are optional — the defaults are applied on the JS side before the native call.
| Parameter | Type | Default | Description |
|---|---|---|---|
uuids |
string[] |
[] |
Proximity UUIDs to filter by. See platform differences below. |
scanDurationMs |
number |
5000 |
Scan duration in milliseconds (must be > 0). |
Returns: BeaconScanResult[] — deduplicated by UUID + major + minor.
| Behaviour | Android | iOS |
|---|---|---|
Empty uuids ([]) |
Wildcard — discovers all nearby iBeacons | Auto-uses paired beacon UUIDs. Rejects with WILDCARD_NOT_SUPPORTED if none are paired. |
Targeted (["UUID-1"]) |
Filters scan results to matching UUIDs | CoreLocation ranging for those UUIDs |
Possible errors:
| Code | Reason |
|---|---|
SCAN_IN_PROGRESS |
Another scan is already running |
INVALID_UUID |
One of the UUID strings is malformed |
INVALID_DURATION |
Duration ≤ 0 |
PERMISSION_DENIED |
Location permission not granted |
WILDCARD_NOT_SUPPORTED |
iOS: empty UUIDs with no paired beacons |
SCAN_CANCELLED |
cancelScan() was called |
const beacons = await ExpoBeacon.scanForBeaconsAsync(
["E2C56DB5-DFFB-48D2-B060-D0F5A71096E0"],
8000,
);scanForEddystonesAsync(scanDurationMs?)
scanForEddystonesAsync(scanDurationMs?: number): Promise<EddystoneScanResult[]>Performs a one-shot Eddystone scan using BLE. Discovers both Eddystone-UID and Eddystone-URL frames.
The parameter is optional — the default is applied on the JS side before the native call.
| Parameter | Type | Default | Description |
|---|---|---|---|
scanDurationMs |
number |
5000 |
Scan duration in milliseconds (must be > 0). |
Returns: EddystoneScanResult[] — deduplicated by namespace:instance (UID) or url (URL).
Possible errors:
| Code | Reason |
|---|---|
SCAN_IN_PROGRESS |
Another Eddystone scan is already running |
INVALID_DURATION |
Duration ≤ 0 |
SCAN_CANCELLED |
cancelScan() was called |
const eddystones = await ExpoBeacon.scanForEddystonesAsync(5000);startContinuousScan()
startContinuousScan(): voidBegins a continuous BLE scan that streams beacon discoveries via events:
onBeaconFound— iBeacon advertisementsonEddystoneFound— Eddystone advertisements
Does not return results directly — subscribe to events before calling. Call stopContinuousScan() to end.
iOS: Only reports iBeacons whose UUID is registered via
pairBeacon(). Eddystones are reported regardless of pairing.
stopContinuousScan()
stopContinuousScan(): voidStops the continuous scan. No-op if no scan is running.
cancelScan()
cancelScan(): voidCancels any in-progress one-shot scan (iBeacon or Eddystone). The pending promise rejects with code SCAN_CANCELLED.
pairBeacon(identifier, uuid, major, minor, name?, timeoutSeconds?)
pairBeacon(identifier: string, uuid: string, major: number, minor: number, name?: string, timeoutSeconds?: number): voidRegisters an iBeacon for persistent monitoring.
| Parameter | Type | Description |
|---|---|---|
identifier |
string |
Unique label (e.g. "lobby-entrance"). Re-using an iBeacon identifier replaces the previous entry. |
uuid |
string |
iBeacon proximity UUID (case-insensitive, e.g. "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0") |
major |
number |
Major value: 0–65535 |
minor |
number |
Minor value: 0–65535 |
name |
string? |
Optional BLE device name for display purposes |
timeoutSeconds |
number? |
Fire onBeaconTimeout once, this many seconds after the beacon exits range. Cancelled if the beacon is seen again first. |
Possible errors: INVALID_UUID, INVALID_MAJOR, INVALID_MINOR, DUPLICATE_IDENTIFIER (identifier already used by a paired Eddystone).
ExpoBeacon.pairBeacon("main-door", "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0", 1, 42);
// With timeout — fires onBeaconTimeout 30 s after the beacon leaves range
ExpoBeacon.pairBeacon("main-door", "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0", 1, 42, undefined, 30);unpairBeacon(identifier)
unpairBeacon(identifier: string): voidRemoves a paired iBeacon. If monitoring is active, the region stops being tracked immediately.
| Parameter | Type | Description |
|---|---|---|
identifier |
string |
The label used when pairing |
ExpoBeacon.unpairBeacon("main-door");getPairedBeacons()
getPairedBeacons(): PairedBeacon[]Returns all currently paired iBeacons from persistent storage.
const paired = ExpoBeacon.getPairedBeacons();
// [{ identifier: "main-door", uuid: "E2C5…", major: 1, minor: 42 }]pairEddystone(identifier, namespace, instance, name?, timeoutSeconds?)
pairEddystone(identifier: string, namespace: string, instance: string, name?: string, timeoutSeconds?: number): voidRegisters an Eddystone-UID beacon for persistent monitoring. The namespace and instance are normalized to lowercase before storage.
| Parameter | Type | Description |
|---|---|---|
identifier |
string |
Unique label (e.g. "meeting-room"). Re-using an Eddystone identifier replaces the previous entry. |
namespace |
string |
10-byte namespace ID as hex string — must be exactly 20 hex characters |
instance |
string |
6-byte instance ID as hex string — must be exactly 12 hex characters |
name |
string? |
Optional BLE device name for display purposes |
timeoutSeconds |
number? |
Fire onEddystoneTimeout once, this many seconds after the beacon exits range. Cancelled if the beacon is seen again first. |
Possible errors: INVALID_NAMESPACE, INVALID_INSTANCE, DUPLICATE_IDENTIFIER (identifier already used by a paired iBeacon).
ExpoBeacon.pairEddystone("meeting-room", "edd1ebeac04e5defa017", "0123456789ab");
// With timeout — fires onEddystoneTimeout 60 s after the beacon leaves range
ExpoBeacon.pairEddystone("meeting-room", "edd1ebeac04e5defa017", "0123456789ab", undefined, 60);unpairEddystone(identifier)
unpairEddystone(identifier: string): voidRemoves a paired Eddystone beacon.
| Parameter | Type | Description |
|---|---|---|
identifier |
string |
The label used when pairing |
ExpoBeacon.unpairEddystone("meeting-room");getPairedEddystones()
getPairedEddystones(): PairedEddystone[]Returns all currently paired Eddystone beacons from persistent storage.
const paired = ExpoBeacon.getPairedEddystones();
// [{ identifier: "meeting-room", namespace: "edd1…", instance: "0123…" }]startMonitoring(options?)
startMonitoring(options?: MonitoringOptions | number): Promise<void>Starts background region monitoring for all paired beacons (iBeacon + Eddystone).
Accepts a MonitoringOptions object, a plain number (shorthand for maxDistance), or nothing.
| Property | Type | Default | Description |
|---|---|---|---|
maxDistance |
number |
undefined |
Distance threshold in metres. onBeaconEnter / onEddystoneEnter only fires when measured distance ≤ this value. onBeaconExit / onEddystoneExit always fires. Omit to disable filtering. |
exitDistance |
number |
maxDistance + min(maxDistance × 0.5, 2.5) |
Distance in metres at which exit events fire. Must be ≥ maxDistance. Creates a hysteresis band between enter and exit thresholds to prevent rapid toggling near the boundary. Only used when maxDistance is set. |
notifications |
NotificationConfig |
undefined |
Notification overrides for this session (persisted). |
What happens on each platform:
| Platform | Mechanism |
|---|---|
| Android | Starts BeaconForegroundService (persistent notification). Survives app backgrounding. Auto-restarts after device reboot via BootReceiver. Scan timing: 1.1 s every 5 s. |
| iOS | Activates CLLocationManager region monitoring (iBeacon) + CoreBluetooth BLE scanning (Eddystone). iOS can wake/relaunch the app on region boundary crossings, even if force-quit. |
Possible errors: PERMISSION_DENIED (Always authorization required on iOS).
// Shorthand — just a distance threshold
await ExpoBeacon.startMonitoring(5);
// Full options with custom exit threshold
await ExpoBeacon.startMonitoring({
maxDistance: 10,
exitDistance: 15, // Exit fires when distance exceeds 15m
notifications: {
beaconEvents: {
enterTitle: "Welcome!",
body: "{identifier} is nearby",
},
},
});
// No distance filter, silent
await ExpoBeacon.startMonitoring({
notifications: { beaconEvents: { enabled: false } },
});
// No options at all — monitor all paired beacons, no distance filter, default notifications
await ExpoBeacon.startMonitoring();stopMonitoring()
stopMonitoring(): Promise<void>Stops all background monitoring. On Android, stops the foreground service. Persisted monitoring options (maxDistance, exitDistance, level, exitTimeoutSeconds, …) are cleared on both platforms.
await ExpoBeacon.stopMonitoring();getMonitoringConfig()
getMonitoringConfig(): MonitoringConfigReturns the current monitoring configuration snapshot, including whether background monitoring is active.
This reads the native monitoring settings currently persisted by the module. Option fields are omitted when they have not been explicitly set.
const config = ExpoBeacon.getMonitoringConfig();
// {
// isMonitoring: true,
// maxDistance: 10,
// exitDistance: 15,
// minRssi: -85,
// level: "all"
// }getMonitoredDeviceState(identifier)
getMonitoredDeviceState(identifier: string): MonitoredDeviceState | nullReturns the current monitoring-state snapshot for a paired iBeacon or Eddystone with the matching identifier.
stateis"entered"or"exited".distanceisnullwhen the device is currently exited or there is no live reading yet.- Returns
nullwhen no paired device matches the identifier.
Identifiers should be unique across all paired monitored devices.
const lobby = ExpoBeacon.getMonitoredDeviceState("lobby-entrance");
// {
// kind: "ibeacon",
// identifier: "lobby-entrance",
// uuid: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0",
// major: 1,
// minor: 100,
// state: "entered",
// distance: 2.4
// }getMonitoredDeviceStates()
getMonitoredDeviceStates(): MonitoredDeviceState[]Returns the current monitoring-state snapshots for all paired monitored devices across iBeacon and Eddystone.
const states = ExpoBeacon.getMonitoredDeviceStates();
// [
// { kind: "ibeacon", identifier: "lobby-entrance", state: "entered", distance: 2.4, ... },
// { kind: "eddystone", identifier: "meeting-room", state: "exited", distance: null, ... }
// ]setNotificationConfig(config)
setNotificationConfig(config: NotificationConfig): voidPersists notification configuration applied to all subsequent monitoring sessions. Survives app restarts.
For one-off overrides, pass notifications inside startMonitoring(options) instead. Monitoring-time notification overrides are merged into the persisted notification config, so omitted beacon or CarPlay sections are preserved.
Use the top-level beacons and carPlay sections to configure those notification streams independently. Legacy keys (beaconEvents, carPlayEvents, foregroundService, channel, carPlayChannel) are still accepted for existing apps.
See NotificationConfig for the full shape.
setBeaconNotificationConfig(config)
setBeaconNotificationConfig(config: BeaconNotificationSettings | BeaconNotificationConfig): voidPersists only beacon notification settings without replacing CarPlay settings. Passing a plain BeaconNotificationConfig is treated as beacons.events.
setCarPlayNotificationConfig(config)
setCarPlayNotificationConfig(config: CarPlayNotificationSettings | CarPlayNotificationConfig): voidPersists only CarPlay / Android Auto notification settings without replacing beacon settings. Passing a plain CarPlayNotificationConfig is treated as carPlay.events.
enableEventLogging()
enableEventLogging(): voidCreates/opens the local SQLite database and starts persisting every beacon event (onBeaconEnter, onBeaconExit, onBeaconDistance, onBeaconTimeout, onBeaconFound, onEddystoneEnter, etc.). Call before startMonitoring() or startContinuousScan().
ExpoBeacon.enableEventLogging();disableEventLogging()
disableEventLogging(): voidStops persisting events. Previously logged data is retained — call clearEventLogs() or destroyEventLogs() to remove it.
ExpoBeacon.disableEventLogging();getEventLogs(options?)
getEventLogs(options?: EventLogQueryOptions): EventLogEntry[]Retrieves logged events from the SQLite database, newest first.
| Property | Type | Default | Description |
|---|---|---|---|
limit |
number |
1000 |
Max rows to return (capped at 10 000) |
eventType |
string |
undefined |
Filter by event name (e.g. "onBeaconEnter") |
sinceTimestamp |
number |
undefined |
Only events with timestamp >= value (ms since epoch) |
Returns: EventLogEntry[]
const logs = ExpoBeacon.getEventLogs({ eventType: "onBeaconEnter", limit: 50 });clearEventLogs()
clearEventLogs(): voidDeletes all rows from the event log table. The database file remains.
ExpoBeacon.clearEventLogs();destroyEventLogs()
destroyEventLogs(): voidDisables logging and deletes the entire SQLite database file.
ExpoBeacon.destroyEventLogs();setApiEndpoint(url, apiKey?, id?)
setApiEndpoint(url: string, apiKey?: string, id?: string): voidConfigures a remote endpoint to which native code POSTs every beacon event — delivery works even when the JS bridge is not active (app backgrounded). The configuration persists until changed.
| Parameter | Type | Description |
|---|---|---|
url |
string |
The API endpoint URL to POST events to. |
apiKey |
string? |
Sent as the X-CSFR-Token header (sic — the header is literally X-CSFR-Token, not X-CSRF-Token). |
id |
string? |
Identifier appended to every forwarded event payload. |
Use getApiEndpoint() to read back the current configuration (each field is null if unset).
Events
Subscribe with ExpoBeacon.addListener(eventName, handler). Always call .remove() on the returned subscription during cleanup.
const sub = ExpoBeacon.addListener("onBeaconEnter", handler);
// Later:
sub.remove();Event Summary
| Event | Trigger | Payload Type |
|---|---|---|
onBeaconEnter |
Paired iBeacon enters range (respects maxDistance) |
BeaconRegionEvent |
onBeaconExit |
Paired iBeacon leaves range (always fires) | BeaconRegionEvent |
onBeaconDistance |
Periodic distance update during monitoring (~1/sec) | BeaconDistanceEvent |
onBeaconFound |
iBeacon detected during continuous scan | BeaconScanResult |
onEddystoneFound |
Eddystone detected during continuous scan | EddystoneScanResult |
onEddystoneEnter |
Paired Eddystone enters range (respects maxDistance) |
EddystoneRegionEvent |
onEddystoneExit |
Paired Eddystone leaves range (always fires) | EddystoneRegionEvent |
onEddystoneDistance |
Periodic Eddystone distance update during monitoring | EddystoneDistanceEvent |
onBeaconTimeout |
Paired iBeacon out of range for configured timeoutSeconds |
BeaconTimeoutEvent |
onEddystoneTimeout |
Paired Eddystone out of range for configured timeoutSeconds |
EddystoneTimeoutEvent |
Event Detail
onBeaconEnter
Fired when the device enters the region of a paired iBeacon. If maxDistance was set, only fires when the measured distance is within the threshold.
ExpoBeacon.addListener("onBeaconEnter", (e) => {
// e.identifier — "lobby-entrance"
// e.uuid — "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0"
// e.major — 1
// e.minor — 100
// e.event — "enter"
// e.distance — 3.2 (metres, or –1 if unavailable)
console.log(`Entered "${e.identifier}" at ~${e.distance.toFixed(1)}m`);
});onBeaconExit
Fired when the device leaves the region. Always fires regardless of maxDistance setting.
ExpoBeacon.addListener("onBeaconExit", (e) => {
console.log(`Left "${e.identifier}"`);
});onBeaconDistance
Fired continuously during monitoring with the latest distance reading. Useful for proximity-based UI.
ExpoBeacon.addListener("onBeaconDistance", (e) => {
// e.identifier, e.uuid, e.major, e.minor, e.distance
updateProximityBar(e.identifier, e.distance);
});onBeaconFound
Fired during startContinuousScan() each time an iBeacon advertisement is received.
ExpoBeacon.addListener("onBeaconFound", (b) => {
console.log(`${b.uuid} ${b.major}/${b.minor} — ${b.distance.toFixed(1)}m RSSI: ${b.rssi}`);
});onEddystoneFound
Fired during startContinuousScan() each time an Eddystone advertisement is received.
ExpoBeacon.addListener("onEddystoneFound", (b) => {
if (b.frameType === "uid") {
console.log(`UID: ${b.namespace}/${b.instance} — ${b.distance.toFixed(1)}m`);
} else {
console.log(`URL: ${b.url} — ${b.distance.toFixed(1)}m`);
}
});onEddystoneEnter
Fired when a paired Eddystone-UID beacon enters range during monitoring.
ExpoBeacon.addListener("onEddystoneEnter", (e) => {
console.log(`Eddystone "${e.identifier}" entered (ns: ${e.namespace})`);
});onEddystoneExit
Fired when a paired Eddystone-UID beacon leaves range.
ExpoBeacon.addListener("onEddystoneExit", (e) => {
console.log(`Eddystone "${e.identifier}" exited`);
});onEddystoneDistance
Fired continuously during monitoring with the latest Eddystone distance reading.
ExpoBeacon.addListener("onEddystoneDistance", (e) => {
console.log(`Eddystone "${e.identifier}" → ${e.distance.toFixed(2)}m`);
});onBeaconTimeout
Fired once, timeoutSeconds after a paired iBeacon exits range (or after BLE readings stop for 60 s). Re-detection cancels the pending timer.
ExpoBeacon.addListener("onBeaconTimeout", (e) => {
// e.identifier — "lobby-entrance"
// e.uuid, e.major, e.minor — beacon identity
// e.distance — usually –1 (the beacon is out of range when this fires)
console.log(`Beacon "${e.identifier}" timeout — out of range for configured duration`);
});onEddystoneTimeout
Fired once, timeoutSeconds after a paired Eddystone exits range (or after BLE readings stop for 60 s). Re-detection cancels the pending timer.
ExpoBeacon.addListener("onEddystoneTimeout", (e) => {
// e.identifier, e.namespace, e.instance — Eddystone identity
// e.distance — usually –1 (the beacon is out of range when this fires)
console.log(`Eddystone "${e.identifier}" timeout`);
});TypeScript Types
All types are exported from the package:
import type {
BeaconScanResult,
PairedBeacon,
BeaconRegionEvent,
BeaconDistanceEvent,
BeaconTimeoutEvent,
EddystoneFrameType,
EddystoneScanResult,
PairedEddystone,
EddystoneRegionEvent,
EddystoneDistanceEvent,
EddystoneTimeoutEvent,
BeaconErrorEvent,
ExpoBeaconModuleEvents,
MonitoringOptions,
NotificationConfig,
BeaconNotificationSettings,
BeaconNotificationConfig,
CarPlayNotificationSettings,
CarPlayNotificationConfig,
ForegroundServiceConfig,
NotificationChannelConfig,
CarPlayChannelConfig,
EventLogQueryOptions,
EventLogEntry,
} from "expo-beacon";BeaconScanResult
Returned by scanForBeaconsAsync() and onBeaconFound.
type BeaconScanResult = {
uuid: string; // Proximity UUID, uppercase (e.g. "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0")
major: number; // 0–65535
minor: number; // 0–65535
rssi: number; // Signal strength in dBm (negative, e.g. –65)
distance: number; // Estimated distance in metres (–1 when unavailable)
txPower: number; // Calibrated TX power. Android only — always 0 on iOS (CoreLocation does not expose it)
};PairedBeacon
Returned by getPairedBeacons().
type PairedBeacon = {
identifier: string; // Your label
uuid: string;
major: number;
minor: number;
name?: string; // Optional BLE device name
timeoutSeconds?: number; // Fires onBeaconTimeout this many seconds after the beacon exits range
};BeaconRegionEvent
Payload for onBeaconEnter / onBeaconExit.
type BeaconRegionEvent = {
identifier: string; // Matches PairedBeacon.identifier
uuid: string;
major: number;
minor: number;
event: "enter" | "exit";
distance: number; // Metres at event time; –1 if unavailable
};BeaconDistanceEvent
Payload for onBeaconDistance.
type BeaconDistanceEvent = {
identifier: string;
uuid: string;
major: number;
minor: number;
distance: number; // Estimated distance in metres
};EddystoneScanResult
Returned by scanForEddystonesAsync() and onEddystoneFound.
type EddystoneScanResult = {
frameType: "uid" | "url";
namespace?: string; // 20 hex chars. Present for UID frames.
instance?: string; // 12 hex chars. Present for UID frames.
url?: string; // Decoded URL. Present for URL frames.
rssi: number;
distance: number;
txPower: number;
};PairedEddystone
Returned by getPairedEddystones().
type PairedEddystone = {
identifier: string;
namespace: string; // 20 hex chars
instance: string; // 12 hex chars
name?: string; // Optional BLE device name
timeoutSeconds?: number; // Fires onEddystoneTimeout this many seconds after the beacon exits range
};EddystoneRegionEvent
Payload for onEddystoneEnter / onEddystoneExit.
type EddystoneRegionEvent = {
identifier: string;
namespace: string;
instance: string;
event: "enter" | "exit";
distance: number; // Metres; –1 if unavailable
};EddystoneDistanceEvent
Payload for onEddystoneDistance.
type EddystoneDistanceEvent = {
identifier: string;
namespace: string;
instance: string;
distance: number;
};MonitoringOptions
Passed to startMonitoring().
type MonitoringOptions = {
maxDistance?: number;
exitDistance?: number;
minRssi?: number;
level?: "all" | "events";
};MonitoringConfig
Returned by getMonitoringConfig().
type MonitoringConfig = {
isMonitoring: boolean;
maxDistance?: number;
exitDistance?: number;
minRssi?: number;
level?: "all" | "events";
notifications?: NotificationConfig;
};MonitoredDeviceState
Returned by getMonitoredDeviceState() and getMonitoredDeviceStates().
type MonitoredDeviceState =
| {
kind: "ibeacon";
identifier: string;
uuid: string;
major: number;
minor: number;
state: "entered" | "exited";
distance: number | null;
}
| {
kind: "eddystone";
identifier: string;
namespace: string;
instance: string;
state: "entered" | "exited";
distance: number | null;
};NotificationConfig
Top-level notification configuration.
type NotificationConfig = {
beacons?: BeaconNotificationSettings;
carPlay?: CarPlayNotificationSettings;
// Legacy aliases, still accepted:
beaconEvents?: BeaconNotificationConfig;
carPlayEvents?: CarPlayNotificationConfig;
foregroundService?: ForegroundServiceConfig;
channel?: NotificationChannelConfig;
carPlayChannel?: CarPlayChannelConfig;
};BeaconNotificationSettings
type BeaconNotificationSettings = {
events?: BeaconNotificationConfig;
foregroundService?: ForegroundServiceConfig; // Android only, title/text/icon only
channel?: NotificationChannelConfig; // Android only, beacon alert channel
};CarPlayNotificationSettings
type CarPlayNotificationSettings = {
events?: CarPlayNotificationConfig;
foregroundService?: ForegroundServiceConfig; // Android only, CarPlay-only title/text/icon
channel?: CarPlayChannelConfig; // Android only, CarPlay alert channel
};BeaconNotificationConfig
type BeaconNotificationConfig = {
enabled?: boolean; // Default: true. Set false to suppress.
enterTitle?: string; // Default: "Beacon Entered"
exitTitle?: string; // Default: "Beacon Exited"
timeoutTitle?: string; // Default: "Beacon Timeout"
body?: string; // Default: "{identifier} region {event}ed"
// Supports {identifier} and {event} placeholders.
sound?: boolean; // iOS only. Default: true
icon?: string; // Android only. Drawable resource name.
};ForegroundServiceConfig
type ForegroundServiceConfig = {
title?: string; // Default: "Beacon Monitoring Active"
text?: string; // Default: "Monitoring for iBeacons in the background"
icon?: string; // Android drawable resource name
};Foreground service notifications always use the dedicated Android channel expo_beacon_foreground_channel, created with low importance and no sound/vibration. This config only changes the notification content.
NotificationChannelConfig
type NotificationChannelConfig = {
name?: string; // Default: "Beacon Monitoring"
description?: string; // Default: "Used for background iBeacon region monitoring"
importance?: "low" | "default" | "high"; // Default: "low"
};CarPlayNotificationConfig
type CarPlayNotificationConfig = {
enabled?: boolean; // Default: true. Set false to suppress.
connectedTitle?: string; // Default: "CarPlay Connected"
disconnectedTitle?: string; // Default: "CarPlay Disconnected"
body?: string; // Default: "CarPlay session {event}"
// Supports {event} and {transport} placeholders.
sound?: boolean; // iOS only. Default: true
icon?: string; // Android only. Drawable resource name.
};CarPlayChannelConfig
type CarPlayChannelConfig = {
name?: string; // Default: "CarPlay / Android Auto"
description?: string; // Default: "CarPlay and Android Auto connect/disconnect notifications"
importance?: "low" | "default" | "high"; // Default: "default"
};BeaconTimeoutEvent
Payload for onBeaconTimeout.
type BeaconTimeoutEvent = {
identifier: string;
uuid: string;
major: number;
minor: number;
distance: number; // Usually –1 (the beacon is out of range when the timeout fires)
};EddystoneTimeoutEvent
Payload for onEddystoneTimeout.
type EddystoneTimeoutEvent = {
identifier: string;
namespace: string;
instance: string;
distance: number; // Usually –1 (the beacon is out of range when the timeout fires)
};EventLogQueryOptions
Passed to `getEve