Complete implementation guide for iOS and Android apps using native OS geofencing + Fencemaker REST API. No SDK required — leverage platform capabilities that run in the background even when your app is killed.
If you're using an AI coding assistant (GitHub Copilot, Cursor, Claude, ChatGPT, etc.) to build your Fencemaker integration, copy this prompt to get context-aware code generation tailored to your app. Fill in the placeholders with your actual values before pasting.
AI assistants generate better code when given complete context. This prompt provides your AI with Fencemaker's API structure, platform-specific requirements, and your app's parameters so it produces working code instead of generic examples that need heavy editing.
I'm integrating Fencemaker's geofencing API into my mobile app. Generate production-ready code following these specifications:
## App Context
- App Name: [YOUR_APP_NAME]
- Platform: [iOS / Android / React Native]
- Language: [Swift / Kotlin / JavaScript/TypeScript]
- Minimum OS Version: [e.g., iOS 15.0 / Android 10 / React Native 0.72]
- Use Case: [e.g., delivery zone alerts, fleet tracking, territory assignment, idle detection]
## Fencemaker API Details
- Base URL: https://fencemaker.app/api/v1
- Authentication: X-API-Key header (from secure storage — never hardcoded)
- Track Endpoint: POST /api/v1/track
- Payload: { "device_id": "string", "lat": number, "lon": number }
- Response: { "events": [{ "type": "entered"/"exited", "territory_code": "ZONE-A", "webhook_fired": true }], "territories_inside": ["uuid"] }
- Territories: GET /api/v1/territories (GeoJSON polygon geometry per territory — extract centroid + bounding radius for CLCircularRegion / GeofencingClient)
- Webhooks fire server-side automatically on each /track ping — entry and exit both fire, exit includes dwell_seconds
## Integration Requirements
1. Fetch territories from GET /api/v1/territories, convert GeoJSON geometry to OS-native circular regions (centroid + bounding radius)
2. Register geofences with native OS APIs:
- iOS: CLLocationManager with CLCircularRegion (max 20 simultaneous)
- Android: GeofencingClient with PendingIntent to BroadcastReceiver (max 100)
- React Native: Use react-native-geofencing or expo-location
3. Handle entry/exit events in background (works when app is killed)
4. POST to /api/v1/track with device_id, lat, lon on each event
5. Request "Always" location permission (iOS) / ACCESS_BACKGROUND_LOCATION (Android)
## Code Generation Guidelines
- Include error handling with try/catch and retry logic for failed API calls
- Store failed events locally (SQLite/AsyncStorage/Core Data) and retry when online
- Implement rate limiting: max 1 POST per territory per 60 seconds (deduplicate rapid-fire events)
- Use secure storage for API key (Keychain/EncryptedSharedPreferences, NOT hardcoded)
- Validate coordinates are in bounds (-90 to 90 lat, -180 to 180 lon) before sending
- Add logging for debugging but sanitize location data in production logs
- For dynamic geofence loading: fetch nearest zones when user moves >10km, keep 20 (iOS) or 30 (Android) registered
## Specific Implementation Request
[DESCRIBE WHAT YOU NEED — Examples:]
- "Generate the complete iOS CLLocationManager delegate implementation with geofence registration and event handling"
- "Build an Android BroadcastReceiver that handles geofence transitions and POSTs to Fencemaker"
- "Create a React Native module that fetches nearby geofences, registers them, and handles entry/exit events"
- "Write the permission request flow for background location on [iOS/Android]"
- "Build a local queue system that stores failed API calls and retries with exponential backoff"
- "Generate a Node.js webhook handler that receives Fencemaker events and triggers SMS alerts when dwell_seconds > 600"
## Output Format
Provide:
1. Complete, runnable code (not snippets — full files)
2. Required dependencies/imports at the top
3. Inline comments explaining non-obvious logic
4. Platform-specific manifest/plist configuration (AndroidManifest.xml, Info.plist)
5. Installation/setup steps if any native modules or permissions are required
## Key Facts About Fencemaker
- dwell_seconds is returned on exit events (time spent inside zone) — authoritative server-side value
- Geofences must be ≥100m radius for reliable detection (GPS accuracy limits)
- Events can be delayed 10-30 seconds from actual boundary crossing (OS + network latency)
- I need this to work in production, not just a proof-of-concept
Generate the code now.
App Name: QuickDeliver
Platform: iOS
Language: Swift
Minimum OS Version: iOS 15.0
Use Case: Alert customers when delivery driver enters their zone
Specific Request: "Generate the complete CLLocationManager setup with dynamic geofence loading that fetches zones within 50km of driver's current position, registers the nearest 20, and refreshes every 10km of movement. Include the delegate methods for handling entry/exit and the POST /track implementation with local queueing for offline scenarios."
App Name: FleetWatch
Platform: Android
Language: Kotlin
Minimum OS Version: Android 10
Use Case: Idle time violations — alert when vehicles stay in loading zones >10 minutes
Specific Request: "Build the GeofencingClient implementation with BroadcastReceiver that handles exit events, calculates dwell time client-side as backup, and POSTs to /track. Include the Foreground Service setup with persistent notification and the two-step permission flow for background location. Handle manufacturer-specific battery optimizations (Xiaomi, Huawei) by prompting user to whitelist the app."
App Name: SalesTracker
Platform: React Native
Language: TypeScript
Minimum OS Version: React Native 0.72, iOS 15+, Android 10+
Use Case: Sales reps track which territory they're currently in
Specific Request: "Create a React Native geofencing module using react-native-geofencing that wraps the native iOS and Android implementations. Include the DeviceEventEmitter listeners for entry/exit events, the fetch + register flow with proximity filtering, and permission requests for both platforms. Export a clean API: initGeofencing(), onTerritoryEnter(callback), onTerritoryExit(callback). Handle the case where user denies permissions gracefully."
Paste this condensed version as a comment at the top of your file — AI will autocomplete with Fencemaker-specific patterns:
// Fencemaker Geofencing Integration
// Base URL: https://fencemaker.app/api/v1
// Auth: X-API-Key header (from env var — never hardcode)
// Track: POST /api/v1/track body: { device_id, lat, lon }
// Territories: GET /api/v1/territories (GeoJSON polygon geometry)
// Webhooks fire server-side on /track — entry: event:"entered", exit adds dwell_seconds
// Platform: [iOS 15+ CLLocationManager / Android GeofencingClient / React Native]
// Requirements: Always location permission, background execution, offline retry queue
// Use case: [YOUR_USE_CASE - e.g., delivery zone alerts]
//
// TODO: Fetch territories, convert GeoJSON centroid + bounding radius to OS native region
// TODO: Handle entry/exit events, POST to /api/v1/track with error handling
// TODO: Store failed API calls in Core Data/Room/AsyncStorage, retry when online
iOS and Android have built-in geofencing capabilities that are battery-efficient, always-on, and system-integrated. An SDK would add unnecessary overhead while duplicating what the OS already does perfectly. This guide shows you how to wire native geofencing to Fencemaker's API for webhook-based automation, exit detection with dwell time, and centralized zone management across your fleet.
Your integration has three components:
The flow:
Device enters zone → OS fires local event → App POSTs to /track → Fencemaker logs + webhooks your server → You trigger business logic (SMS, status update, billing, etc.)
iOS: You must request Always location permission (not just When In Use). Apple's App Store review requires clear justification text in your Info.plist explaining why your app needs background location.
Android: You need ACCESS_BACKGROUND_LOCATION permission on Android 10+ and must handle the two-step permission flow (foreground first, then background). A Foreground Service is recommended for real-time tracking scenarios.
fm_live_...)You have three options:
Go to Dashboard → Geofences → Draw on Map → Save with name and metadata
If you have existing zones in another tool (Google Maps, Mapbox, Excel with lat/lon), export as GeoJSON and upload:
bashcurl -X POST https://api.fencemaker.app/v1/geofences/import \
-H "Authorization: Bearer fm_live_your_key_here" \
-F "file=@zones.geojson"
Programmatically define zones from your backend:
bashcurl -X POST https://api.fencemaker.app/v1/geofences \
-H "Authorization: Bearer fm_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"name": "Downtown Delivery Zone",
"geometry": {
"type": "Polygon",
"coordinates": [[
[-122.4194, 37.7749],
[-122.4194, 37.7849],
[-122.4094, 37.7849],
[-122.4094, 37.7749],
[-122.4194, 37.7749]
]]
},
"metadata": {
"zone_type": "delivery",
"priority": "high"
}
}'
Response: You'll get back a fence_id (e.g., fence_abc123). Store this ID — you'll use it when registering the zone with the OS.
At app launch (or periodically), fetch your active geofences from Fencemaker and register them with the OS:
swiftimport Foundation
import CoreLocation
func fetchAndRegisterGeofences() {
let apiKey = "fm_live_your_key_here"
let url = URL(string: "https://api.fencemaker.app/v1/geofences")!
var request = URLRequest(url: url)
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else {
print("Error fetching geofences: \(error?.localizedDescription ?? "Unknown")")
return
}
do {
let geofences = try JSONDecoder().decode(GeofenceResponse.self, from: data)
self.registerGeofencesWithOS(geofences: geofences.data)
} catch {
print("JSON decode error: \(error)")
}
}.resume()
}
func registerGeofencesWithOS(geofences: [Geofence]) {
let locationManager = CLLocationManager()
// Remove old geofences
for region in locationManager.monitoredRegions {
locationManager.stopMonitoring(for: region)
}
// Register new ones (iOS limit: 20 simultaneous geofences)
for fence in geofences.prefix(20) {
let center = CLLocationCoordinate2D(
latitude: fence.center.lat,
longitude: fence.center.lon
)
let region = CLCircularRegion(
center: center,
radius: fence.radius,
identifier: fence.id // Use Fencemaker's fence_id as identifier
)
region.notifyOnEntry = true
region.notifyOnExit = true
locationManager.startMonitoring(for: region)
}
}
// Data models
struct GeofenceResponse: Codable {
let data: [Geofence]
}
struct Geofence: Codable {
let id: String
let name: String
let center: Coordinate
let radius: Double
}
struct Coordinate: Codable {
let lat: Double
let lon: Double
}
kotlinimport com.google.android.gms.location.*
import okhttp3.*
import org.json.JSONObject
fun fetchAndRegisterGeofences() {
val apiKey = "fm_live_your_key_here"
val client = OkHttpClient()
val request = Request.Builder()
.url("https://api.fencemaker.app/v1/geofences")
.addHeader("Authorization", "Bearer $apiKey")
.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.e("Fencemaker", "Fetch failed: ${e.message}")
}
override fun onResponse(call: Call, response: Response) {
val json = JSONObject(response.body?.string() ?: "{}")
val geofences = json.getJSONArray("data")
registerGeofencesWithOS(geofences)
}
})
}
fun registerGeofencesWithOS(geofences: JSONArray) {
val geofencingClient = LocationServices.getGeofencingClient(context)
val geofenceList = mutableListOf()
for (i in 0 until geofences.length()) {
val fence = geofences.getJSONObject(i)
val center = fence.getJSONObject("center")
geofenceList.add(
Geofence.Builder()
.setRequestId(fence.getString("id")) // Use Fencemaker fence_id
.setCircularRegion(
center.getDouble("lat"),
center.getDouble("lon"),
fence.getDouble("radius").toFloat()
)
.setExpirationDuration(Geofence.NEVER_EXPIRE)
.setTransitionTypes(
Geofence.GEOFENCE_TRANSITION_ENTER or
Geofence.GEOFENCE_TRANSITION_EXIT
)
.build()
)
}
val geofencingRequest = GeofencingRequest.Builder()
.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
.addGeofences(geofenceList)
.build()
val pendingIntent = getGeofencePendingIntent()
geofencingClient.addGeofences(geofencingRequest, pendingIntent)
}
javascript// Install: npm install react-native-geolocation-service
// Install: npm install @react-native-community/geolocation (legacy)
// OR use Expo Location: expo install expo-location
import Geolocation from 'react-native-geolocation-service';
// For React Native CLI, also install react-native-geofencing (community package)
// npm install react-native-geofencing
const API_KEY = 'fm_live_your_key_here';
async function fetchAndRegisterGeofences() {
try {
const response = await fetch('https://api.fencemaker.app/v1/geofences', {
headers: {
'Authorization': `Bearer ${API_KEY}`,
},
});
const data = await response.json();
const geofences = data.data;
// Get current location to filter nearby geofences only
Geolocation.getCurrentPosition(
(position) => {
const userLat = position.coords.latitude;
const userLon = position.coords.longitude;
// Filter: only register geofences within 50km of current location
const nearbyGeofences = geofences.filter(fence => {
const distance = calculateDistance(
userLat, userLon,
fence.center.lat, fence.center.lon
);
return distance < 50000; // 50km in meters
});
registerGeofencesWithOS(nearbyGeofences.slice(0, 20)); // iOS limit: 20
},
(error) => console.error('Location error:', error),
{ enableHighAccuracy: true }
);
} catch (error) {
console.error('Fetch geofences error:', error);
}
}
// Haversine distance formula
function calculateDistance(lat1, lon1, lat2, lon2) {
const R = 6371000; // Earth radius in meters
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLon = (lon2 - lon1) * Math.PI / 180;
const a =
Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLon/2) * Math.sin(dLon/2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
return R * c;
}
function registerGeofencesWithOS(geofences) {
// Using react-native-geofencing (community package)
const ReactNativeGeofencing = require('react-native-geofencing');
// Remove old geofences
ReactNativeGeofencing.removeAll();
// Register new ones
geofences.forEach(fence => {
ReactNativeGeofencing.addGeofence({
id: fence.id,
latitude: fence.center.lat,
longitude: fence.center.lon,
radius: fence.radius,
notifyOnEntry: true,
notifyOnExit: true,
});
});
// Start monitoring
ReactNativeGeofencing.startMonitoring()
.then(() => console.log('Geofencing started'))
.catch(err => console.error('Geofencing error:', err));
}
Note: React Native requires native modules for geofencing. Popular options:
react-native-geofencing (community package, good for basic use cases)react-native-background-geolocation (commercial, $$$, but production-grade)expo-location (if using Expo, has geofencing support in bare workflow)All require ejecting from Expo managed workflow and linking native modules. Check their docs for platform-specific setup (AndroidManifest.xml permissions, Info.plist keys, Podfile changes).
iOS: Maximum 20 simultaneously monitored geofences per app. If you have more zones, prioritize by proximity or business logic.
Android: Maximum 100 geofences per app, but battery impact scales with count. Recommend keeping under 30 active at once.
If you have hundreds or thousands of zones (warehouse network, franchise locations, service territories), you can't register them all at once. The solution: only register geofences near the user's current location, and refresh dynamically as they move.
?near_lat={lat}&near_lon={lon}&radius=50000 to get zones within 50kmswiftimport CoreLocation
class DynamicGeofenceManager: NSObject, CLLocationManagerDelegate {
let locationManager = CLLocationManager()
var lastUpdateLocation: CLLocation?
let updateThresholdMeters: CLLocationDistance = 10000 // 10km
func startDynamicGeofencing() {
locationManager.delegate = self
locationManager.startUpdatingLocation()
locationManager.allowsBackgroundLocationUpdates = true
}
func locationManager(_ manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]) {
guard let currentLocation = locations.last else { return }
// Check if we've moved far enough to refresh geofences
if let lastLocation = lastUpdateLocation {
let distance = currentLocation.distance(from: lastLocation)
if distance < updateThresholdMeters {
return // No refresh needed
}
}
// Fetch nearby geofences and re-register
fetchNearbyGeofences(
lat: currentLocation.coordinate.latitude,
lon: currentLocation.coordinate.longitude
)
lastUpdateLocation = currentLocation
}
func fetchNearbyGeofences(lat: Double, lon: Double) {
let apiKey = "fm_live_your_key_here"
let radius = 50000 // 50km in meters
let url = URL(string: "https://api.fencemaker.app/v1/geofences?near_lat=\(lat)&near_lon=\(lon)&radius=\(radius)")!
var request = URLRequest(url: url)
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else { return }
do {
let result = try JSONDecoder().decode(GeofenceResponse.self, from: data)
// Sort by distance, take nearest 20
let nearestFences = result.data
.sorted { fence1, fence2 in
let dist1 = self.distance(
from: (lat, lon),
to: (fence1.center.lat, fence1.center.lon)
)
let dist2 = self.distance(
from: (lat, lon),
to: (fence2.center.lat, fence2.center.lon)
)
return dist1 < dist2
}
.prefix(20)
self.registerGeofencesWithOS(geofences: Array(nearestFences))
} catch {
print("Decode error: \(error)")
}
}.resume()
}
func distance(from: (Double, Double), to: (Double, Double)) -> Double {
let location1 = CLLocation(latitude: from.0, longitude: from.1)
let location2 = CLLocation(latitude: to.0, longitude: to.1)
return location1.distance(from: location2)
}
func registerGeofencesWithOS(geofences: [Geofence]) {
// Remove old geofences
for region in locationManager.monitoredRegions {
locationManager.stopMonitoring(for: region)
}
// Register new ones
for fence in geofences {
let center = CLLocationCoordinate2D(
latitude: fence.center.lat,
longitude: fence.center.lon
)
let region = CLCircularRegion(
center: center,
radius: fence.radius,
identifier: fence.id
)
region.notifyOnEntry = true
region.notifyOnExit = true
locationManager.startMonitoring(for: region)
}
print("✓ Registered \(geofences.count) nearby geofences")
}
}
bashGET https://api.fencemaker.app/v1/geofences?near_lat=37.7749&near_lon=-122.4194&radius=50000
# Response: Returns only geofences within 50km of (37.7749, -122.4194)
# Sorted by distance from query point
significantLocationChanges (iOS) or fused location provider (Android) instead of high-frequency GPS pollingMost real-world apps have concentrated zones (warehouse clusters, city delivery areas, franchise territories). A driver in San Francisco doesn't need geofences registered for New York. By dynamically loading only nearby zones, you can support unlimited total geofences while staying under platform limits.
When the OS fires a geofence event, your app receives a callback. Here's where you POST to Fencemaker's /track endpoint.
swiftimport CoreLocation
class LocationDelegate: NSObject, CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager,
didEnterRegion region: CLRegion) {
if let circularRegion = region as? CLCircularRegion {
sendEventToFencemaker(
fenceId: circularRegion.identifier,
eventType: "entry",
location: circularRegion.center
)
}
}
func locationManager(_ manager: CLLocationManager,
didExitRegion region: CLRegion) {
if let circularRegion = region as? CLCircularRegion {
sendEventToFencemaker(
fenceId: circularRegion.identifier,
eventType: "exit",
location: circularRegion.center
)
}
}
func sendEventToFencemaker(fenceId: String,
eventType: String,
location: CLLocationCoordinate2D) {
let apiKey = "fm_live_your_key_here"
let deviceId = UIDevice.current.identifierForVendor?.uuidString ?? "unknown"
let url = URL(string: "https://api.fencemaker.app/v1/track")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let payload: [String: Any] = [
"device_id": deviceId,
"fence_id": fenceId,
"event_type": eventType,
"latitude": location.latitude,
"longitude": location.longitude,
"timestamp": ISO8601DateFormatter().string(from: Date())
]
request.httpBody = try? JSONSerialization.data(withJSONObject: payload)
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
print("Track error: \(error)")
} else {
print("✓ \(eventType) event logged for fence \(fenceId)")
}
}.resume()
}
}
kotlinimport android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.google.android.gms.location.Geofence
import com.google.android.gms.location.GeofencingEvent
import okhttp3.*
import org.json.JSONObject
import java.util.UUID
class GeofenceBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val geofencingEvent = GeofencingEvent.fromIntent(intent)
if (geofencingEvent.hasError()) {
Log.e("Geofence", "Error: ${geofencingEvent.errorCode}")
return
}
val geofenceTransition = geofencingEvent.geofenceTransition
val triggeringGeofences = geofencingEvent.triggeringGeofences
triggeringGeofences.forEach { geofence ->
val eventType = when (geofenceTransition) {
Geofence.GEOFENCE_TRANSITION_ENTER -> "entry"
Geofence.GEOFENCE_TRANSITION_EXIT -> "exit"
else -> return
}
sendEventToFencemaker(
context = context,
fenceId = geofence.requestId,
eventType = eventType,
location = geofencingEvent.triggeringLocation
)
}
}
private fun sendEventToFencemaker(
context: Context,
fenceId: String,
eventType: String,
location: Location?
) {
val apiKey = "fm_live_your_key_here"
val deviceId = Settings.Secure.getString(
context.contentResolver,
Settings.Secure.ANDROID_ID
)
val client = OkHttpClient()
val payload = JSONObject().apply {
put("device_id", deviceId)
put("fence_id", fenceId)
put("event_type", eventType)
put("latitude", location?.latitude ?: 0.0)
put("longitude", location?.longitude ?: 0.0)
put("timestamp", System.currentTimeMillis())
}
val body = RequestBody.create(
MediaType.parse("application/json"),
payload.toString()
)
val request = Request.Builder()
.url("https://api.fencemaker.app/v1/track")
.addHeader("Authorization", "Bearer $apiKey")
.post(body)
.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.e("Fencemaker", "Track failed: ${e.message}")
}
override fun onResponse(call: Call, response: Response) {
Log.i("Fencemaker", "✓ $eventType logged for fence $fenceId")
}
})
}
}
AndroidManifest.xml:
xml<receiver android:name=".GeofenceBroadcastReceiver"
android:exported="false" />
javascriptimport { DeviceEventEmitter, Platform } from 'react-native';
import DeviceInfo from 'react-native-device-info';
const API_KEY = 'fm_live_your_key_here';
// Set up geofence event listeners
function setupGeofenceListeners() {
const ReactNativeGeofencing = require('react-native-geofencing');
// Listen for geofence events
DeviceEventEmitter.addListener('geofenceEnter', (event) => {
console.log('Entered geofence:', event.id);
sendEventToFencemaker(event.id, 'entry', event.latitude, event.longitude);
});
DeviceEventEmitter.addListener('geofenceExit', (event) => {
console.log('Exited geofence:', event.id);
sendEventToFencemaker(event.id, 'exit', event.latitude, event.longitude);
});
}
async function sendEventToFencemaker(fenceId, eventType, latitude, longitude) {
try {
const deviceId = await DeviceInfo.getUniqueId();
const response = await fetch('https://api.fencemaker.app/v1/track', {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
device_id: deviceId,
fence_id: fenceId,
event_type: eventType,
latitude: latitude,
longitude: longitude,
timestamp: new Date().toISOString(),
}),
});
if (response.ok) {
console.log(`✓ ${eventType} event logged for fence ${fenceId}`);
} else {
console.error('Track API error:', response.status);
}
} catch (error) {
console.error('Send event error:', error);
// TODO: Store failed events locally and retry later
}
}
// Call this in your App.js or index.js
setupGeofenceListeners();
Permissions Setup (iOS - Info.plist):
xml<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>We monitor delivery zones in the background</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>We need your location for zone tracking</string>
<key>UIBackgroundModes</key>
<array>
<string>location</string>
</array>
Permissions Setup (Android - AndroidManifest.xml):
xml<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
Request Permissions at Runtime:
javascriptimport { PermissionsAndroid, Platform } from 'react-native';
async function requestLocationPermissions() {
if (Platform.OS === 'android') {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION
);
if (granted === PermissionsAndroid.RESULTS.GRANTED) {
// On Android 10+, request background separately
if (Platform.Version >= 29) {
await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.ACCESS_BACKGROUND_LOCATION
);
}
}
}
// iOS: Use react-native-permissions or handle via Info.plist
}
Every time a device hits POST /track, Fencemaker fires a webhook to your server. This is where your business logic lives — send SMS, update order status, trigger billing, etc.
Dashboard → Settings → Webhook URL → Enter https://yourdomain.com/fencemaker-webhook
json{
"event_type": "entry",
"fence_id": "fence_abc123",
"fence_name": "Downtown Delivery Zone",
"device_id": "d7f8e9a1-2b3c-4d5e-6f7g-8h9i0j1k2l3m",
"latitude": 37.7749,
"longitude": -122.4194,
"timestamp": "2026-05-23T14:32:18Z",
"metadata": {
"zone_type": "delivery",
"priority": "high"
}
}
json{
"event_type": "exit",
"fence_id": "fence_abc123",
"fence_name": "Downtown Delivery Zone",
"device_id": "d7f8e9a1-2b3c-4d5e-6f7g-8h9i0j1k2l3m",
"latitude": 37.7755,
"longitude": -122.4201,
"timestamp": "2026-05-23T14:47:33Z",
"dwell_seconds": 915,
"metadata": {
"zone_type": "delivery",
"priority": "high"
}
}
Key field: dwell_seconds is always included on exit events — the number of seconds between entry and exit. Use this for idle time violations, loading zone enforcement, or time-based billing.
javascriptconst express = require('express');
const app = express();
app.use(express.json());
app.post('/fencemaker-webhook', async (req, res) => {
const { event_type, fence_id, fence_name, device_id, dwell_seconds } = req.body;
console.log(`${event_type} event: Device ${device_id} ${event_type} ${fence_name}`);
if (event_type === 'exit' && dwell_seconds > 600) {
// Vehicle stayed > 10 minutes — trigger alert
await sendAlert(device_id, `Idle time violation: ${dwell_seconds}s in ${fence_name}`);
}
if (event_type === 'entry' && fence_id === 'fence_delivery_zone_1') {
// Driver entered delivery zone — update order status
await updateOrderStatus(device_id, 'out_for_delivery');
}
res.status(200).send('OK');
});
async function sendAlert(deviceId, message) {
// Your SMS/push notification logic here
console.log(`Alert for ${deviceId}: ${message}`);
}
async function updateOrderStatus(deviceId, status) {
// Your order management system update
console.log(`Updated ${deviceId} status to ${status}`);
}
app.listen(3000, () => console.log('Webhook server running on port 3000'));
Fencemaker signs all webhook requests with an HMAC signature in the X-Fencemaker-Signature header. Verify this in production to ensure requests are legitimate. Implementation guide: fencemaker.app/docs/webhook-security
This is the most critical step — without proper permissions, geofencing won't work when your app is backgrounded or killed.
xml<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>We need background location to detect when you enter/exit delivery zones and trigger automated notifications</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>We need your location to track deliveries and monitor zones</string>
<key>UIBackgroundModes</key>
<array>
<string>location</string>
</array>
swiftlet locationManager = CLLocationManager()
// First request "When In Use"
locationManager.requestWhenInUseAuthorization()
// After user grants "When In Use", request "Always"
// Show this in context (e.g., after user completes onboarding)
locationManager.requestAlwaysAuthorization()
Apple requires a clear explanation of why you need Always permission. Your app description and screenshots should demonstrate the core functionality (delivery tracking, zone alerts, etc.). Generic "we need location" justifications will be rejected.
xml<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
kotlin// Step 1: Request foreground location first
val foregroundPermissions = arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
ActivityCompat.requestPermissions(
this,
foregroundPermissions,
LOCATION_PERMISSION_REQUEST_CODE
)
// Step 2: After foreground is granted, request background
// (Android 10+ requires this as a separate step)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_BACKGROUND_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION),
BACKGROUND_LOCATION_REQUEST_CODE
)
}
}
If you need continuous location updates (not just geofence events), use a Foreground Service:
kotlinclass LocationTrackingService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Tracking Active")
.setContentText("Monitoring delivery zones")
.setSmallIcon(R.drawable.ic_location)
.build()
startForeground(NOTIFICATION_ID, notification)
// Start location updates here
return START_STICKY
}
}
AndroidManifest.xml:
xml<service android:name=".LocationTrackingService"
android:foregroundServiceType="location"
android:exported="false" />
| Use Case | Implementation Pattern | Key Webhook Logic |
|---|---|---|
| Delivery Zone Entry | Customer defines serviceability zones; driver app monitors entry | On entry webhook: send customer SMS "Driver is nearby" |
| Territory Assignment | Sales reps assigned to geographic territories; lead comes in | On lead creation, check point-in-fence via GET /check; assign to rep |
| Idle Time Violations | Fleet vehicles monitored for excessive dwell in loading zones | On exit webhook: if dwell_seconds > 600, flag vehicle + alert dispatcher |
| Route Compliance | Define approved transit corridors; drivers must stay inside | On exit webhook from corridor fence: trigger compliance alert |
| Dynamic Pricing | Higher delivery fees for zones outside city center | At checkout, check customer address via GET /check; apply zone-based fee from metadata |
When monitoring 10-20 geofences using CLCircularRegion
When using GeofencingClient (not continuous location updates)
Typical delay from physical boundary crossing to webhook delivery
/track with an array of events.Geofencing runs in the background with elevated permissions. A bug here can drain batteries, spam your API, or violate user privacy. Check these items before shipping to production:
X-Fencemaker-Signature HMAC header to prevent spoofingdevice_id + fence_id + event_type with 60-second windowCLLocationManager.locationServicesEnabled() (iOS) or LocationManager.isProviderEnabled() (Android) and prompt user if off| Platform | Check This | Why It Matters |
|---|---|---|
| iOS | Info.plist usage descriptions are clear and specific | Apple rejects apps with generic "we need location" text — explain the specific feature (e.g., "delivery zone alerts") |
| iOS | App works with "Precise Location" toggle OFF | Users can disable precise location in iOS 14+ — geofences still work but with ~1km accuracy (test this) |
| Android | Foreground Service notification is not dismissable | If using Foreground Service, notification must be ongoing — user can't swipe it away or location updates stop |
| Android | Test on Xiaomi, Huawei, Samsung devices | Manufacturer battery savers kill background location aggressively — prompt user to whitelist your app in battery settings |
| React Native | Native module linking is complete | Run pod install (iOS) and rebuild native code after installing geofencing libraries — common source of "module not found" crashes |
CLLocationManager.authorizationStatus — must be .authorizedAlwaysregion.notifyOnEntry and region.notifyOnExit are both truesetInitialTrigger(INITIAL_TRIGGER_ENTER) to fire immediately if already inside a zoneadb shell am broadcast -a com.google.android.gms.location.Geofence to simulate eventscurl -X POST yourdomain.com/webhook -d '{"test": true}'