Mobile Integration Guide

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.

🤖 AI Coding Assistant

AI Integration Assistant Prompt

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.

Why This Helps

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.

Copy and paste this prompt into your AI assistant
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.

Customization Examples

Example 1 — iOS Swift Developer Building a Delivery App

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."

Example 2 — Android Kotlin Developer Building a Fleet Tracker

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."

Example 3 — React Native Developer Building a Territory Tracker

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."

Tips for Better AI Output

📌 Be specific about your use case "Idle time detection for loading zones" gets better code than "geofencing"
📋 Request full files, not snippets AI generates better code when it has full context of imports and class structure
🔁 Iterate in steps Start with fetch + register, test, then add event handling, then offline queueing
🔍 Mention edge cases upfront "Handle offline mode", "deduplicate rapid events", "work on Xiaomi devices"
⚠️

Always Review AI-Generated Code Before Shipping

  • Test thoroughly on real devices (simulators miss battery/accuracy issues)
  • Verify API endpoints and payload shapes match Fencemaker's actual API
  • Check that API keys are in secure storage, never hardcoded in source
  • Test background execution: force-quit the app and cross a geofence boundary
  • Review the "Before You Ship" checklist below in this guide

Alternative: Inline IDE Comment (Copilot / Cursor)

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

Need more prompts? See the full AI Prompts library — 14 prompts covering iOS, Android, React Native, Flutter, use cases, and optimization patterns.

Inspired by Twilio's Docs AI Buddy prompts

Why No SDK?

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.

Architecture Overview

Your integration has three components:

  1. Native OS Geofencing — iOS CLLocationManager or Android GeofencingClient monitors zones locally on-device
  2. Your App Code — Receives entry/exit events, calls Fencemaker REST API to log the event
  3. Fencemaker Backend — Receives POST /track, fires webhooks to your server with fence_id, device_id, event type, and dwell_seconds on exit

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.)

Background Permissions Required

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.

Step 1: Get Your API Key & Define Zones

Create Account + API Key

  1. Sign up at fencemaker.app
  2. Go to Dashboard → API Keys → Generate New Key
  3. Copy your key (starts with fm_live_...)

Define Your Geofences

You have three options:

Option A: Manual via Dashboard

Go to Dashboard → Geofences → Draw on Map → Save with name and metadata

Option B: Import GeoJSON

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"

Option C: Create via API

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.

Step 2: Fetch Geofences in Your App

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:

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).

Platform Limits

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.

Dynamic Geofence Loading (Bypass Platform Limits)

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.

Strategy

  1. Fetch user's current position
  2. Call Fencemaker API with ?near_lat={lat}&near_lon={lon}&radius=50000 to get zones within 50km
  3. Register the nearest 20 geofences (iOS) or 30 geofences (Android) with the OS
  4. Every 5-10 minutes (or when user moves >10km), repeat steps 1-3 to refresh the active set

Implementation Example (iOS)

swiftimport 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")
    }
}

API Endpoint (Near Query)

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

Best Practices for Dynamic Loading

Why This Works

Most 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.

Step 3: Handle Entry/Exit Events

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
}

Step 4: Configure Webhook Endpoint (Server-Side)

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.

Set Your Webhook URL

Dashboard → Settings → Webhook URL → Enter https://yourdomain.com/fencemaker-webhook

Webhook Payload (Entry)

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"
  }
}

Webhook Payload (Exit with Dwell Time)

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.

Example Webhook Handler (Node.js/Express)

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'));

Webhook Security

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

Step 5: Request Background Location Permissions

This is the most critical step — without proper permissions, geofencing won't work when your app is backgrounded or killed.

Info.plist Configuration

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>

Request Permission in Code

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()

App Store Review

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.

AndroidManifest.xml

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" />

Request Permissions (Two-Step Flow)

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
        )
    }
}

Foreground Service (Recommended for Real-Time Tracking)

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" />

Common Use Cases

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

Performance & Battery Optimization

iOS Battery Impact
<1%

When monitoring 10-20 geofences using CLCircularRegion

Android Battery Impact
<2%

When using GeofencingClient (not continuous location updates)

Event Latency
10-30s

Typical delay from physical boundary crossing to webhook delivery

Best Practices

Before You Ship: Code Review Checklist

⚠️ Critical: Review This Before Production

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:

API Key & Security

Background Behavior

Edge Cases

Data Integrity

Production Readiness

Platform-Specific Gotchas

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

Troubleshooting

iOS: Geofences Not Firing

Android: Events Delayed or Missing

Webhooks Not Arriving

API Reference Quick Links