LAANC Deep Linking

Overview

The AirMap mobile apps support dynamic deep-linking that allows third-party web, iOS, and Android apps to link directly to the AirMap flight creation flow and receive authorization if available. This enables third-party apps to request controlled airspace authorization through the AirMap app as part of the FAA LAANC program (where available). Upon flight submission, the third-party app can also be called back with details of the flight, including the status of requests for airspace authorization.

Support

Feel free to Contact Us with questions about deep linking or for a technical checkout prior to launch.

Integration Steps

1. Check for LAANC enabled airspace

Use either the Status API or Advisory API to check if LAANC authorization is available for a given area. The Advisory API is preferable, as it is the newer, contextual version of the Status API and will eventually replace it.

Advisory API

Use the Airspace Advisory API to pass in a polygon geometry and query the area for controlled airspace:

https://api.airmap.com/advisory/v1/airspace

Name
Type
Description

geometry

string

GeoJSON Polygon geometry

rulesets

CSV string of rulesets

rules (use usa_part_107)

For example, we can check the airspace around San Jose Airport. If we input usa_part_107 rules, that will ensure we receive controlled airspace in the area.

curl "https://api.airmap.com/advisory/v1/airspace?geometry=%7B%20%22type%22:%20%22Polygon%22,%20%22coordinates%22:%20%5B%20%5B%20%5B%20-121.96884155273436,%2037.351601144954785%20%5D,%20%5B%20-121.97708129882812,%2037.33850000215498%20%5D,%20%5B%20-121.95785522460936,%2037.323212446730174%20%5D,%20%5B%20-121.94412231445314,%2037.33850000215498%20%5D,%20%5B%20-121.96884155273436,%2037.351601144954785%20%5D%20%5D%20%5D%20%7D&rulesets=usa_part_107,usa_ama" \
     -H 'X-API-Key: <API_KEY>'

And the response:

{
  "status": "success",
  "data": {
    "color": "orange",
    "advisories": [
      {
        "id": "6178604",
        "name": "SAN JOSE AIRPORT CLASS C  requires FAA authorization, immediate authorization available at or below 400 ft",
        "last_updated": "2017-11-17T21:16:18.000Z",
        "latitude": 37.2916794,
        "longitude": -121.9750252,
        "distance": 5736,
        "type": "controlled_airspace",
        "city": "San Jose",
        "state": "California",
        "country": "USA",
        "rule_id": 5554,
        "ruleset_id": "usa_part_107",
        "properties": {
          "url": "https://www.airmap.com/controlled-airspace-authorization-laanc/",
          "laanc": true,
          "ceiling": 400,
          "airport_id": "SJC",
          "airport_name": "Norman Y Mineta San Jose Intl",
          "authorization": true,
          "last_edit_date": "8/11/2017",
          "airspace_classification": "C",
          "type": "c"
        },
        "color": "orange",
        "requirements": {
          "notice": {
            "phone": null,
            "digital": false
          }
        }
      },
      {
        "id": "5986138",
        "name": "SAN JOSE AIRPORT CLASS C  requires FAA authorization, immediate authorization available at or below 200 ft",
        "last_updated": "2017-11-17T21:16:18.000Z",
        "latitude": 37.3083464,
        "longitude": -121.9083598,
        "distance": 6028,
        "type": "controlled_airspace",
        "city": "San Jose",
        "state": "California",
        "country": "USA",
        "rule_id": 5554,
        "ruleset_id": "usa_part_107",
        "properties": {
          "url": "https://www.airmap.com/controlled-airspace-authorization-laanc/",
          "laanc": true,
          "ceiling": 400,
          "airport_id": "SJC",
          "airport_name": "Norman Y Mineta San Jose Intl",
          "authorization": true,
          "last_edit_date": "8/11/2017",
          "airspace_classification": "C",
          "type": "c"
        },
        "color": "orange",
        "requirements": {
          "notice": {
            "phone": null,
            "digital": false
          }
        }
      },
      {
        "id": "6178602",
        "name": "SAN JOSE AIRPORT CLASS C  requires FAA authorization, immediate authorization available at or below 300 ft",
        "last_updated": "2017-11-17T21:16:18.000Z",
        "latitude": 37.3916795,
        "longitude": -121.8916929,
        "distance": 7032,
        "type": "controlled_airspace",
        "city": "San Jose",
        "state": "California",
        "country": "USA",
        "rule_id": 5554,
        "ruleset_id": "usa_part_107",
        "properties": {
          "url": "https://www.airmap.com/controlled-airspace-authorization-laanc/",
          "laanc": true,
          "ceiling": 400,
          "airport_id": "SJC",
          "airport_name": "Norman Y Mineta San Jose Intl",
          "authorization": true,
          "last_edit_date": "8/11/2017",
          "airspace_classification": "C",
          "type": "c"
        },
        "color": "orange",
        "requirements": {
          "notice": {
            "phone": null,
            "digital": false
          }
        }
      }
    ]
  }
}

To check if the airport is part of LAANC and authorization is available through AirMap, check that both laanc and authorization are true (under properties). If this is the case, the flight can be authorized through AirMap.

Status API

Use the Status API to pass in a lat/lon and receive controlled airspace in that area:

https://api.airmap.com/status/v2/point

Name
Type
Description

latitude

float

latitude

longitude

float

longitude

types

CSV string of airspace types

buffer

integer

radius from lat/lon to include in airspace check

For example, we can check the airspace around San Jose Airport. Make sure to include controlled_airspace as a type (and any other types that are of interest, like TFRs). We should also keep the buffer as small as possible that still covers the flight area.

curl "https://api.airmap.com/status/v2/point?latitude=37.342&longitude=-121.942&types=controlled_airspace&buffer=500" \
     -H 'X-API-Key: <API_KEY>'

And we'll receive all the controlled airspace in that area, spilt up into the FAA facility map grids. Here we receive only one grid.

{
  "status": "success",
  "data": {
    "max_safe_distance": 0,
    "advisory_color": "yellow",
    "advisories": [
      {
        "id": "dee00629-e991-4ddb-81aa-e45b3e190310",
        "name": "SAN JOSE AIRPORT CLASS C requires FAA authorization, immediate authorization available below 200 ft",
        "last_updated": "2017-10-31T12:58:24.000Z",
        "latitude": 37.3083464,
        "longitude": -121.9083598,
        "distance": 0,
        "type": "controlled_airspace",
        "city": "San Jose",
        "state": "California",
        "country": "USA",
        "properties": {
          "url": "https://www.faa.gov/uas/request_waiver/",
          "laanc": true,
          "ceiling": 400,
          "airport_id": "SJC",
          "airport_name": "Norman Y Mineta San Jose Intl",
          "authorization": true,
          "last_edit_date": "8/11/2017",
          "airspace_classification": "C",
          "type": "c",
          "airspace_class": "c"
        },
        "color": "yellow"
      }
    ]
  }
}

To check if the airport is part of LAANC and authorization is available through AirMap, we should check that both laanc and authorization are true (under properties). If this is the case, the flight can get authorization through AirMap.

2. Getting authorization through the AirMap app

We have defined a custom URL scheme so that your app can send information to the AirMap app by using specific URLs. This works for both the iOS and Android AirMap apps.

You can create links from within your app that allow you to link to the flight creation page in the AirMap app. In case the AirMap app isn't installed, the link will open the Apple App Store or Google Play Store. Once the installs the app, the app will continue to the flight creation screen. If the link is opened on a desktop, it will redirect to an informational page.

The base app link is https://xjy5t.app.goo.gl and the query parameters are specified below. If you include a callback_url, the AirMap app can automatically return to a specific point in your app with some state information. Otherwise, the user will have to manually return to your app.

AirMap Link

The AirMap link is a URL that is a parameter in the Deeplink URL below - but it also has its own parameters, shown in the table below:

https://app.airmap.io/create_flight/v1

Parameters
Description
Value

geometry

Flight geometry query parameter in GeoJSON format

Example:
{"geometry":{"type":"Polygon","coordinates":[[[-121.94532394409178,37.344777918311429],[-121.93824291229248,37.344777918311429],[-121.93824291229248,37.349110739090591],[-121.94532394409178,37.349110739090591],[-121.94532394409178,37.344777918311429]]]}}

callback_url

(Optional)
The url that the AirMap app will call when the Flight Creation Process is completed. We will return a flight_id, and an array of authorizations.

See Authorization below for more details

myapp://airmap_created_flight/?authorizations=%5B%7B%22description%22:%22Automatic%20authorization%20to%20fly%20in%20controlled%20airspace%22,%22status%22:%22accepted%22,%22message%22:%22FAA%20confirmation%20number:%20ARMZWLTQW%22,%22authority%22:%7B%22id%22:%22faa-laanc%22,%22name%22:%22FAA%22%7D%7D,%7B%22description%22:%22manual%20authorization%20to%20fly.%22,%22status%22:%22pending%22,%22message%22:%22%22,%22authority%22:%7B%22id%22:%22airmap-amd%22,%22name%22:%22Delano%22%7D%7D,%7B%22description%22:%22manual%20authorization%20to%20fly.%22,%22status%22:%22pending%22,%22message%22:%22%22,%22authority%22:%7B%22id%22:%22airmap-amd%22,%22name%22:%22Whatcom%20County%22%7D%7D,%7B%22description%22:%22manual%20authorization%20to%20fly.%22,%22status%22:%22pending%22,%22message%22:%22%22,%22authority%22:%7B%22id%22:%22airmap-amd%22,%22name%22:%22Cumming%22%7D%7D%5D&flight_id=flight%7CBmB5qZKTZAbbkEIgWQKqJFvYMkk3

altitude

(Optional)
The maximum altitude AGL in meters that will be pre-filled in the AirMap flight creation UI. The provided value will be converted and displayed as either metric or imperial based on the user's locale and snap to known presets in the application (e.g. 25, 50, 100, etc)

Float (Meters)

buffer

(Optional)
The buffer around a given operating area's geometry that will be pre-filled in the AirMap flight creation UI. Only applicable for Path (LineString) and Point geometries. The provided value will be converted and displayed as either metric or imperial based on the user's locale and snap to known presets in the application (e.g. 25, 50, 100, etc)

Float (Meters)

ruleset_ids

(Optional)
A comma-separated list of ruleset identifiers to enable in the AirMap application. If none are provided, a user's preference or default rulesets will be enabled.

CSV String:
usa_part_107

Deeplink URL

Parameters
Description
Value

link

Required
See table above
URI encoded link with flight geometry query parameter in GeoJSON format and callback url. This will ensure the AirMap app opens with a flight created in the right area.

https://app.airmap.io/create_flight/v1/?geometry=<GEOJSON_GEOMETRY>&callback_url=<YOUR_CALLBACK>

apn

Required - Do not modify

com.airmap.airmap

isi

Required - Do not modify

1042824733

ibi

Required - Do not modify

com.airmap.AirMap

ofl

Required - Do not modify

https://www.airmap.com/airspace-authorization/

efr

Required - Do not modify

1

utm_source

Required - Do not modify

partner

utm_medium

Required - Do not modify

deeplink

utm_campaign

Required - Do not modify

laanc

utm_content

Insert the iTunes ID or Google Play ID of your app

<YOUR_APP_ID>

ct

iOS App Only
Required - Do not modify

laanc

mt

iOS App Only
Required - Do not modify

8

pt

iOS App Only
Required - Do not modify

117967485

Authorization Status

If you include a callback_url, an encoded JSON array of authorizations will be sent back that will look something similar to this:

[
  {
    "description": "Automatic authorization to fly in controlled airspace",
    "status": "accepted",
    "message": "FAA confirmation number: ARMZWLTQW",
    "authority": {
      "id": "faa-laanc",
      "name": "FAA"
    }
  }
]

The table below lists the possible values and descriptions for the status of an authorization:

Status
Description

pending

The request with the authority has been made and a response is pending

accepted

The request with the authority has been accepted

rejected

The request with the authority has been rejected

not_requested

The request with the authority has not been requested

cancelled

The request with the authority has been cancelled

The JSON authorization array can be decoded using the AirMap Swift SDK:

let flightId = params.first(where: { $0.name == "flight_id" })?.value
let authorizationsJSON = params.first(where: { $0.name == "authorizations" })?.value
let authorizations = [AirMapFlightBriefing.Authorization](JSONString: authorizationsJSON ?? "")

Example

The example below will open the AirMap Flight Plan Form if opened on a mobile device (or redirect to app store if the AirMap app isn't installed):

  // Construct a GeoJSON point object
  let geoJSON: [String: Any] = [
    "geometry": [
      "type": "Point",
      "coordinates": [
        -121.94446563720703,
         37.293310828693805
      ]
    ]
  ]

  // Convert the GeoJSON object to data and string
  let geoJSONData = try! JSONSerialization.data(withJSONObject: geoJSON, options: [])
  let geoJSONString = String(bytes: geoJSONData, encoding: .utf8)!

  // Construct a URL that will launch the AirMap app and create a flight with the passed parameters.If the AirMap app
  // is not already installed, launch the App Store. Once the user installs and launches the app, the flight creation continues automatically.
  // This link is specific to iOS callers
  var dynamicLink = URLComponents(string: "https://xjy5t.app.goo.gl/?apn=com.airmap.airmap&isi=1042824733&ibi=com.airmap.AirMap&efr=1&ofl=https://www.airmap.com/airspace-authorization/&utm_source=partner&utm_medium=deeplink&utm_campaign=laanc")!

  dynamicLink.queryItems = dynamicLink.queryItems! + [
    URLQueryItem(
      name: "utm_content",
      // Your iTunes app identifier (numeric id, not bundle id)
      value: "<YOUR_iTUNES_ID>"
    )
  ]

  // Construct the URL that is handed off to the AirMap app for deep linking
  var deepLink = URLComponents(string: "https://app.airmap.io/create_flight/v1/")!

  deepLink.queryItems = [
    // Pass the flight geometry
    URLQueryItem(
      name: "geometry",
      value: geoJSONString
    ),
    // Pass a csv of ruleset identifiers
    URLQueryItem(
      name: "ruleset_ids",
      value: "usa_part_107"
    ),
    // Flight altitude in meters (= 100 feet)
    URLQueryItem(
      name: "altitude",
      value: 30.48
    ),

    URLQueryItem(
      name: "ruleset_ids",
      value: "usa_part_107"
    ),

    // Pass a callback that your app implements to handle status of flight creation and authorization
    URLQueryItem(
      name: "callback_url",
      value: "myapp://airmap_created_flight/"
    )
  ]

  // Add the deep link to the dynamic link
  dynamicLink.queryItems = dynamicLink.queryItems! + [
    URLQueryItem(name: "link", value: deepLink.url!.absoluteString)
  ]

  let dynamicURL = dynamicLink.url!

  // Ask the system to open the URL
  UIApplication.shared.open(dynamicURL, options: [:]) { (succeeded) in
    if succeeded {
      print("Success")
    } else {
      print("Could not open URL")
    }
  }

String pointGeoJSON = "{\"type\":\"Feature\",\"geometry\":{\"type\":\"Point\",\"coordinates\":[-121.94313526153564,37.337544621080653]},\"properties\":null}";

Uri deepLink = new Uri.Builder()
        .scheme("https")
        .authority("www.airmap.com")
        .path("create_flight/v1/")
        .appendQueryParameter("geometry", pointGeoJSON)
        .appendQueryParameter("altitude", "25")
        .appendQueryParameter("buffer", "20")
        .appendQueryParameter("callback_url", YOUR_SCHEME_NAME + "://airmap_flight_created/")
        .build();

Uri dynamicLink = Uri.parse("https://xjy5t.app.goo.gl/?apn=com.airmap.airmap&isi=1042824733&ibi=com.airmap.AirMap&efr=1&ofl=https://www.airmap.com/airspace-authorization/&utm_source=partner&utm_medium=deeplink&utm_campaign=laanc")
        .buildUpon()
        .appendQueryParameter("utm_content", YOUR_ITUNES_ID)
        .appendQueryParameter("link", deepLink.toString())
        .build();


Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(dynamicLink);
startActivity(intent);
var geojson = {
  "type": "Polygon",
  "coordinates": [
    [
      [-121.96884155273436, 37.351601144954785],
      [-121.97708129882812, 37.33850000215498],
      [-121.95785522460936, 37.323212446730174],
      [-121.94412231445314, 37.33850000215498],
      [-121.96884155273436, 37.351601144954785]
    ]
  ]
}

function createLink(geojson) {

  var url = "https://xjy5t.app.goo.gl/?";

  var params = {
    link: "https://app.airmap.io/create_flight/v1/?geometry=" + encodeURIComponent(JSON.stringify({ "geometry": geojson })),
    apn: "com.airmap.airmap",
    isi: 1042824733,
    ibi: "com.airmap.AirMap",
    ofl: "https://www.airmap.com/airspace-authorization/",
    efr: 1,
    utm_source: "partner",
    utm_medium: "deeplink",
    utm_campaign: "laanc",
    utm_content: "<APP_ID>"
  }
  url += ($.param(params));

  //console.log(url)
  window.open(url)

  return url
}

This will generate a dynamic link like this:

https://xjy5t.app.goo.gl/?link=https%3A%2F%2Fapp.airmap.io%2Fcreate_flight%2Fv1%2F%3Fgeometry%3D%257B%2522geometry%2522%253A%257B%2522type%2522%253A%2522Polygon%2522%252C%2522coordinates%2522%253A%255B%255B%255B-121.96884155273436%252C37.351601144954785%255D%252C%255B-121.97708129882812%252C37.33850000215498%255D%252C%255B-121.95785522460936%252C37.323212446730174%255D%252C%255B-121.94412231445314%252C37.33850000215498%255D%252C%255B-121.96884155273436%252C37.351601144954785%255D%255D%255D%257D%257D&apn=com.airmap.airmap&isi=1042824733&ibi=com.airmap.AirMap&ofl=https%3A%2F%2Fwww.airmap.com%2Fairspace-authorization%2F&efr=1&utm_source=partner&utm_medium=deeplink&utm_campaign=link&utm_content=%3CMy_app_id%3E

Sample Link

If a callback_url is provided, we can then decode and process the authorizations parameter in the callback:

import AirMap

class AppDelegate: UIResponder, UIApplicationDelegate {

	func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
        
        guard
            let components = URLComponents(string: url.absoluteString),
            let params = components.queryItems else { return false }

        if url.host == "airmap_created_flight" {
            
            let flightId = params.first(where: { $0.name == "flight_id" })?.value
            let authorizationsJSON = params.first(where: { $0.name == "authorizations" })?.value
            let authorizations = [AirMapFlightBriefing.Authorization](JSONString: authorizationsJSON ?? "")

            print("AirMap flight : ", flightId ?? "–")
            
            if let authorizations = authorizations {
                for authorization in authorizations {
                    print("\n")
                    print("Authority     : ", authorization.authority.name)
                    print("Description   : ", authorization.description)
                    print("Status        : ", authorization.status.description)
                    print("Message       : ", authorization.message)
                }
            } else {
                print("** No Authorizations returned **")
            }
        }
        
        return true
    }    
}
Intent intent = getIntent();
if (Intent.ACTION_VIEW.equals(intent.getAction())) {
    Uri uri = getIntent().getData();

    String flightId = uri.getQueryParameter("flight_id");

    String json = uri.getQueryParameter("authorizations");
    if (!TextUtils.isEmpty(json)) {
        try {
            JSONArray jsonArray = new JSONArray(json);

            for (int i = 0; i < jsonArray.length(); i++) {
                JSONObject jsonObject = jsonArray.getJSONObject(i);

                AirMapAuthorization authorization = new AirMapAuthorization(jsonObject);
                Log.d(TAG, "Authority: " + authorization.getAuthority().getName());
                Log.d(TAG, "Status: " + authorization.getStatus());
                Log.d(TAG, "Description: " + authorization.getDescription());
                Log.d(TAG, "Message: " + authorization.getMessage());
            }
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }
}