> ## Documentation Index
> Fetch the complete documentation index at: https://docs.appdna.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Maps

> Configure the map provider and keys once per app, and what each key is for.

Onboarding flows can contain a **Map** element — a route through several stops, or a single place
with its details. Before it can draw anything, the app needs a map provider and a key.

All of it is configured **once per app**, not per flow and not per map:

**Settings → Apps → *your app* → Maps**

## Choose a provider

|                      | Mapbox                  | Google Maps                        |
| :------------------- | :---------------------- | :--------------------------------- |
| Key you need         | Public token (`pk.…`)   | Maps API key (`AIza…`)             |
| Static maps          | ✅                       | ✅                                  |
| Pan & zoom on device | —                       | ✅                                  |
| Custom styling       | Mapbox Studio style URL | Styled-map JSON, or a Cloud map ID |
| Light / dark themes  | ✅                       | ✅                                  |

The provider applies to **every** Map element in every flow for this app. It sits next to the keys
because it selects between them: picking a provider you have no key for is the one mistake that
produces a placeholder on every device with nothing to explain it, so the settings page warns you
inline when the two disagree.

<Info>
  Both keys are **yours**. Maps are fetched directly by the device and by the flow editor's preview,
  so usage bills to your own Mapbox or Google account. There is deliberately no shared AppDNA key:
  Mapbox's Product Terms forbid us serving their imagery on another account's behalf, and Google
  bills map requests to whoever owns the key.
</Info>

## Mapbox public token

Create one under **Account → Tokens** at [mapbox.com](https://account.mapbox.com/access-tokens/),
then paste it into the Mapbox field.

It must be a **public** token — it starts with `pk.`. It travels inside every map image URL, so it
is public by design; restrict it by URL in the Mapbox dashboard rather than trying to keep it
secret. A secret `sk.` token is rejected by the image API and must never be shipped to devices.

## Google Maps API key

In the [Google Cloud console](https://console.cloud.google.com/apis/credentials), create an API key
and enable the APIs you need:

| Enable this              | For                      |
| :----------------------- | :----------------------- |
| **Maps Static API**      | The map image — required |
| **Maps SDK for iOS**     | Pan & zoom on iOS        |
| **Maps SDK for Android** | Pan & zoom on Android    |

Paste the key (it starts with `AIza`) into the Google Maps field.

<Info>
  The **API restrictions** dropdown only lists APIs that are already **enabled** in the project. If it
  looks empty, enable the ones above in the Library first, then come back to the key — the list
  refreshes. Key changes can take a few minutes to take effect, so an immediate `403` is not
  necessarily a mistake.
</Info>

<Warning>
  **Restrict it by API, not by application.** A Google key accepts only **one** application
  restriction — None, HTTP referrers, IP addresses, Android apps, or iOS apps — and the SDK fetches
  the static map as a plain HTTPS request straight from the device. That request carries no referrer
  and no bundle ID, so an HTTP-referrer restriction works in the console preview and **fails on every
  device**, while an iOS/Android app restriction covers pan & zoom but not the static image.

  Set **Application restrictions: None**, then use **API restrictions → Restrict key** and select only
  the APIs in the table above. Add a cap under **APIs & Services → Quotas** so a leaked key cannot
  run up a bill. That is the combination that works on device and in the console.
</Warning>

## Google Places key — a different key

The **Google Places API key** on the same page is not the same credential and is entirely optional.
It powers place search when you add stops to a map in the editor. It is used only by the console,
never shipped to devices, and stored encrypted — so it is never displayed again after you save it.
Without it you can still enter coordinates by hand.

|                                  | Google **Maps** key | Google **Places** key |
| :------------------------------- | :------------------ | :-------------------- |
| Draws the map                    | ✅                   | —                     |
| Searches for stops in the editor | —                   | ✅                     |
| Sent to devices                  | ✅ (it has to be)    | Never                 |
| Readable after saving            | ✅                   | No, write-only        |

## Pan & zoom

Turning on **Pan & zoom** for a Map element gives the user a real, draggable map instead of a
still image. It needs the Google provider, and on **Android** it also needs one line in your app's
manifest, because the Maps SDK for Android reads its key at build time:

```xml theme={null}
<application>
    <meta-data
        android:name="com.google.android.geo.API_KEY"
        android:value="YOUR_ANDROID_MAPS_API_KEY" />
</application>
```

iOS needs nothing for the **static** map — its Maps SDK takes the key at runtime, so the key you set
here is enough. For **pan & zoom**, an iOS app that integrates via Swift Package Manager must also
add the GoogleMaps package to its own app target (CocoaPods hosts, including React Native and
Flutter, do not need to).
See the [Android](/sdks/android/installation#interactive-maps-optional) and
[iOS](/sdks/ios/installation#interactive-maps) installation guides for the full detail, including
the CocoaPods linkage settings React Native and Flutter hosts need.

Without that manifest line an Android app falls back to the still image — the map simply does not
pan. Nothing breaks.

## When a map shows "Map unavailable"

That placeholder is the deliberate, labelled fallback: a map with no key degrades to a caption,
never to a blank box. It appears when:

| Cause                                  | Fix                                                                         |
| :------------------------------------- | :-------------------------------------------------------------------------- |
| No key for the selected provider       | Add the matching key above — the settings page warns you when they disagree |
| Key present but the API is not enabled | Enable **Maps Static API** in Google Cloud                                  |
| Key restricted too tightly             | Allow your app and, for the editor preview, the console's domain            |
| A Mapbox `sk.` secret token was pasted | Replace it with the `pk.` public token                                      |

The flow editor's preview uses the same keys and the same provider as the device, so whatever you
see on the canvas is what your users will see.
