Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
APIs to organize maps
Projects help you organize maps and manage team permissions.
With these APIs, you can manage the projects in your workspace.
APIs to build dashboards
With these APIs, you can create and edit a layer's components.
APIs to visualize spatial data
Layers enable you to visualize, style and interact with your spatial data.
With these APIs, you can upload data, manage layer styling, publish and refresh live data layers.
APIs to upload data
With these APIs, you can upload your data to create new layers.
There are a variety of ways to interact with Felt’s modern GIS platform outside of the user interface. They can be grouped into two buckets: tools for programmatically creating and modifying maps, and tools for building custom experiences for map viewers. These tools can be used to solve distinct challenges and also be used in tandem with one another.
Felt’s REST API allows editors to interact with the Felt platform via code, performing actions such as creating new maps, adding data to maps, styling layers, and more. The REST API can be leveraged from any environment that is capable of sending GET and POST requests.
For Python users, interactions with the REST API are simplified through the felt-python module, which can be installed with pip and used to call the REST API endpoints directly from Python functions.
Felt’s user interface allows a large amount of customization, offering the ability to generate complex cartographic designs, adding components to create a dashboard, and much more.
However, sometimes application developers need further control over the experience of viewing and/or interacting with a map. For example, they may want to run custom logic after a user clicks on a feature in a layer, or animate data on the map based on other types of user input elsewhere on the webpage. For these situations and many more, Felt’s JavaScript SDK allows developers to programmatically control maps in two ways: Extensions let you write custom code directly within Felt maps, with access to all SDK functionality. Alternatively, you can Felt maps into your own applications and use the SDK to control the embedded map experience.
All calls to the Felt API authenticate with a personal access token sent as a Bearer token in the request header:
Authorization: Bearer <API Token>Tokens start with the felt_pat_ prefix. Since a token grants access to your account, store it securely — treat it like a password. Prefer environment variables or a secrets manager over hardcoding tokens in source code.
You can create an API token in the Developers tab of the Workspace Settings page:
Be sure to take note of the token before closing the dialog; you won't have a second chance to view it. Only a hash of your token is stored on Felt's servers.
Workspace-scoped. Each token belongs to the workspace it was created in and can only access that workspace's resources. Requests against another workspace's resources fail with 401.
They act as you. Requests made with your token have your account's permissions in that workspace.
No expiry. Tokens remain valid until revoked. To rotate a token, create a new one in workspace settings, switch your integration over, and revoke the old one.
Failed authentication returns 401 with a JSON body explaining the problem — see .
A successful response returns your user details, confirming the token works:
From here, head to to create your first map via the API.
APIs for building maps
Maps are the centerpiece of Felt.
With these APIs, you can create, retrieve, update, delete, move, and duplicate maps programmatically.
APIs to export layer data
With these APIs, you can export data to CSV, GeoJSON, and other formats.
APIs to connect your data
Sources connect your databases to Felt.
With these APIs, you can configure data source connections, credentials, and sync settings to create live maps.
APIs to share maps securely
Embed tokens enable safely sharing your private maps.
With these APIs, you can generate secure tokens for embedding maps.
APIs to export map images
With these APIs, you can render a map to a PNG, JPG or PDF image.
APIs for user information
Users represent the people in your workspace.
With these APIs, you can retrieve user profile information.
APIs for drawing spatially
Elements enable you to annotate maps with custom shapes, text, and markers.
With these APIs, you can create, update, and delete map elements.
APIs for programatic collaboration
Comments bring conversations to mapping.
With these APIs, you can export, resolve, and delete map comments and collaboration threads.
References have been updated in the app and documentation, while naming in the REST API and JS SDK remains unchanged. See Working with annotations and Drawing annotations for more.



# This looks like:
# FELT_API_TOKEN="felt_pat_ABCDEFUDQPAGGNBmX40YNhkCRvvLI3f8/BCwD/g8"
FELT_API_TOKEN="<YOUR_API_TOKEN>"
curl \
-H "Authorization: Bearer ${FELT_API_TOKEN}" \
"https://felt.com/api/v2/user"import requests
# This looks like:
# api_token = "felt_pat_ABCDEFUDQPAGGNBmX40YNhkCRvvLI3f8/BCwD/g8"
api_token = "<YOUR_API_TOKEN>"
r = requests.get(
"https://felt.com/api/v2/user",
headers={"Authorization": f"Bearer {api_token}"}
)
assert r.ok
print(r.json()){
"id": "AbC123dEfG456hIjK789lM",
"type": "user",
"name": "Your Name",
"email": "you@example.com"
}Workspaces are the place where users in the same organization collaborate and share maps. A user may form part of several workspaces but, at the very least, always forms part of one.
API tokens are created per-workspace. If you wish to interact with several workspaces via the Felt API, you must create a different API token for each one.
Creating a new map is as simple as making a POST request to the maps endpoint.
# Your API token should look like this:
# FELT_API_TOKEN="felt_pat_ABCDEFUDQPAGGNBmX40YNhkCRvvLI3f8/BCwD/g8"
FELT_API_TOKEN="
import requests
# Your API token should look like this:
# api_token = "felt_pat_ABCDEFUDQPAGGNBmX40YNhkCRvvLI3f8/BCwD/g8"
import os
from felt_python import create_map
# Setting your API token as an env variable can save
# you from repeating it in every function call
os.environ["FELT_API_TOKEN"] = "<YOUR_API_TOKEN>"
response = create_map(
title="My newly created map",
lat=40,
lon=-3,
public_access="private",
)
map_id = response["id"]You should see a response like this (trimmed for brevity):
{
"id": "CjU1CMJPTAGofjOK3ICf1D",
"type": "map",
"title": "My newly created map",
"url": "https://felt.com/map/My-newly-created-map-CjU1CMJPTAGofjOK3ICf1D",
"public_access": "view_only",
"layers": [],
"created_at": "2024-05-25T15:51:34"
}Notice the "id" property. Every map has a unique ID, which is also part of the map's URL. Let's take note of it for future API calls.
Also part of the response is a "url" property, which is the URL to your newly-created map.
Performing a GET request to a map URL will give you useful information about that map, including title, URL, layers, thumbnail URL, creation and visited timestamps.
To remove a map from your workspace, simply perform a DELETE request to the map's URL:
To move a map to a different folder or project, send a POST request to the map's move URL with either a project_id or a folder_id in the body. You can find project IDs by listing your projects with GET /api/v2/projects:
The easiest way of uploading data into a Felt map via the API is to import from a URL. Here's an example importing all the recent earthquakes from the USGS' live GeoJSON feed:
# Your API token and map ID should look like this:
# FELT_API_TOKEN="felt_pat_ABCDEFUDQPAGGNBmX40YNhkCRvvLI3f8/BCwD/g8"
# MAP_ID="CjU1CMJPTAGofjOK3ICf1D"
import requests
# Your API token should look like this:
# api_token = "felt_pat_ABCDEFUDQPAGGNBmX40YNhkCRvvLI3f8/BCwD/g8"
import os
from felt_python import upload_url
# Setting your API token as an env variable can save
# you from repeating it in every function call
os.environ["FELT_API_TOKEN"] = "<YOUR_API_TOKEN>"
map_id = "<YOUR_MAP_ID>"
url_upload = upload_url(
map_id=map_id,
layer_url="https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_hour.geojson",
layer_name="USGS Earthquakes",
)
layer_id = url_upload["layer_id"]Like maps, layers also have unique identifiers. Make sure to take note of them for subsequent calls, like styling a layer or removing it.
Files aren't uploaded to the Felt app — instead, they're uploaded directly to Amazon S3. Therefore, creating a layer from a file on your computer is a two-step process:
Perform a POST request to receive an S3 presigned URL which you can later upload your files to:
r = requests.post(
f"https://felt.com/api/v2/maps/{map_id}/upload",
headers={
"Authorization": f"Bearer {api_token}",
"Content-Type": "application/json",
},
json={"name": "My new layer"},
)
assert r.ok
layer_id = r.json()["layer_id"]
presigned_upload = r.json()from felt_python import upload_file
file_name = "<YOUR_FILE_WITH_EXTENSION>" # Example: regions.geojson
upload_file(
map_id=map_id,
file_name=file_name,
layer_name="My new layer",
)You can check the upload status of a layer by querying it. The response includes two useful fields:
progress — a number from 0 to 100, the percentage complete. Typed as a float in the API spec, so don't assume an integer.
status — one of uploading, processing, completed, or failed.
Poll until status is completed (or failed). A layer must reach completed before it can be styled or .
APIs to publish layers
With these APIs, you can publish your layers to your workspace library.
The Felt REST API allows you to programmatically interact with the Felt platform, enabling you to integrate Felt's powerful mapping capabilities into your own workflows and pipelines.
You are able to create and manipulate Maps, Layers, Annotations, Sources, Projects and more.
The REST API is available on select . Reach out to to learn more or set up a trial.
All Felt API endpoints are hosted at the following base URL:
All calls to the Felt API must be authenticated. The easiest way to authenticate your API calls is by creating an API token and providing it as a Bearer token in the request header.
You can create an API token in the :
Learn more about API tokens here:
The easiest way to interact with the Felt API is by using our
curl \
-H "Authorization: Bearer ${FELT_API_TOKEN}" \
"https://felt.com/api/v2/maps/${MAP_ID}"r = requests.get(
f"https://felt.com/api/v2/maps/{map_id}",
headers={"Authorization": f"Bearer {api_token}"}
)
assert r.ok
print(r.json())from felt_python import get_map
get_map(map_id)curl \
-X DELETE \
-H "Authorization: Bearer ${FELT_API_TOKEN}" \
"https://felt.com/api/v2/maps/${MAP_ID}"r = requests.delete(
f"https://felt.com/api/v2/maps/{map_id}",
headers={"Authorization": f"Bearer {api_token}"}
)
assert r.okfrom felt_python import delete_map
delete_map(map_id)curl \
-X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${FELT_API_TOKEN}" \
"https://felt.com/api/v2/maps/${MAP_ID}/move" \
-d "{\"project_id\": \"${PROJECT_ID}\"}"r = requests.post(
f"https://felt.com/api/v2/maps/{map_id}/move",
json={"project_id": project_id},
headers={"Authorization": f"Bearer {api_token}"}
)
assert r.ok
print(r.json())from felt_python import move_map
move_map(map_id, project_id)# This code is a continuation of the previous Python code block
# and assumes you already have a "presigned_upload" variable
file_name = "<YOUR_FILE_WITH_EXTENSION>" # Example: regions.geojson
url = presigned_upload["url"]
presigned_attributes = presigned_upload["presigned_attributes"]
# A 204 response indicates that the upload was successful
with open(file_name, "rb") as file_obj:
output = requests.post(
url,
# Order is important, file should come at the end
files={**presigned_attributes, "file": file_obj},
)# Nothing! Uploading a file is a single step with the felt-python librarycurl \
"https://felt.com/api/v2/maps/${MAP_ID}/layers/${LAYER_ID}" \
-H "Authorization: Bearer ${FELT_API_TOKEN}"r = requests.get(
f"https://felt.com/api/v2/maps/{map_id}/layers/{layer_id}",
headers={"Authorization": f"Bearer {api_token}"},
)
assert r.ok
print(r.json()["progress"])from felt_python import get_layer_details
get_layer_details(map_id, layer_id)["progress"]felt-pythonFor map management beyond creation — reading details, deleting, moving between projects — see Navigating maps and workspaces.
Creating a new map is as simple as making a POST request to the maps endpoint.
# Your API token should look like this:
# FELT_API_TOKEN="felt_pat_ABCDEFUDQPAGGNBmX40YNhkCRvvLI3f8/BCwD/g8"
FELT_API_TOKEN="<YOUR_API_TOKEN>"
curl \
-X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${FELT_API_TOKEN}" \
"https://felt.com/api/v2/maps" \
-d '{"title": "My newly created map"}'import requests
# This looks like:
# api_token = "felt_pat_ABCDEFUDQPAGGNBmX40YNhkCRvvLI3f8/BCwD/g8"
api_token = "<YOUR_API_TOKEN>"
r = requests.post(
"https://felt.com/api/v2/maps",
json={"title": "My newly created map"},
headers={"Authorization": f"Bearer {api_token}"}
)
assert r.ok
map_id = r.json()["id"]
print(r.json())import os
from felt_python import create_map
# Setting your API token as an env variable can save
# you from repeating it in every function call
os.environ["FELT_API_TOKEN"] = "<YOUR_API_TOKEN>"
response = create_map(
title="My newly created map",
lat=40,
lon=-3,
)
map_id = response["id"]You should see a response like this (trimmed for brevity):
Notice the "id" property. Every map has a unique ID, which is also part of the map's URL. Let's take note of it for future API calls.
Also part of the response is a "url" property, which is the URL to your newly-created map. Feel free to open it! For now, it should just show a blank map.
This example uploads from a URL; for file uploads and monitoring processing, see Uploading files and URLs.
Now that we've created a new map, let's add some data to it. We'll need the map_id included in the previous call's response.
Felt supports many kinds of file and URL imports. In this case, we'll import all the recent earthquakes from the USGS' live GeoJSON feed:
# Store the map ID from the previous call:
# MAP_ID="CjU1CMJPTAGofjOK3ICf1D"
MAP_ID="<YOUR_MAP_ID>"
curl \
-X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${FELT_API_TOKEN}" \
"https://felt.com/api/v2/maps/${MAP_ID}/upload" \
-d '{"import_url":"https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_hour.geojson", "name": "USGS Earthquakes"}'r = requests.post(
f"https://felt.com/api/v2/maps/{map_id}/upload",
json={
"import_url":"https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_hour.geojson",
"name": "USGS Earthquakes",
},
headers={"Authorization": f"Bearer {api_token}"}
)
assert r.ok
layer_id = r.json()["layer_id"]
print(r.json())from felt_python import upload_url
url_upload = upload_url(
map_id=map_id,
layer_url="https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_hour.geojson",
layer_name="USGS Earthquakes",
)
layer_id = url_upload["layer_id"]Like maps, layers also have unique identifiers. Let's take note of this one (the layer_id field in the response) so we can style it in the next call.
You can see the uploaded result in your map:
For reading a layer's current style and more styling patterns, see Styling layers.
Layers can be styled at upload time or afterwards. Let's change the style of our newly-created earthquakes layer so that points are bigger and in green color:
# Store the layer ID from the previous call:
# LAYER_ID="F1XyzAB2TQi9CDeFgHiJkL"
LAYER_ID="<YOUR_LAYER_ID>"
curl \
-X POST \
"https://felt.com/api/v2/maps/${MAP_ID}/layers/${LAYER_ID}/update_style" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${FELT_API_TOKEN}" \
--data '{"style": {"paint": {"color": "green", "opacity": 0.9, "size": 30, "strokeColor": "auto", "strokeWidth": 1}, "legend": {}, "type": "simple", "version": "2.3.1"}}'new_fsl = {
"paint": {
"color": "green",
"opacity": 0.9,
"size": 30,
"strokeColor": "auto",
"strokeWidth": 1
},
"legend": {},
"type": "simple",
"version": "2.3.1"
}
r = requests.post(
f"https://felt.com/api/v2/maps/{map_id}/layers/{layer_id}/update_style",
json={"style": new_fsl},
headers={"Authorization": f"Bearer {api_token}"}
)
assert r.ok
print(r.json())from felt_python import update_layer_style
new_fsl = {
"paint": {
"color": "green",
"opacity": 0.9,
"size": 30,
"strokeColor": "auto",
"strokeWidth": 1
},
"legend": {},
"type": "simple",
"version": "2.3.1"
}
update_layer_style(
map_id=map_id,
layer_id=layer_id,
style=new_fsl,
)Go to your map to see how your new layer looks:
For refreshing file-based layers and the full refresh flow, see Refreshing live data layers.
Similar to a URL upload, refreshing an existing URL layer is just a matter of making a single POST request:
curl \
-X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${FELT_API_TOKEN}" \
"https://felt.com/api/v2/maps/${MAP_ID}/layers/${LAYER_ID}/refresh"r = requests.post(
f"https://felt.com/api/v2/maps/{map_id}/layers/{layer_id}/refresh",
headers={"Authorization": f"Bearer {api_token}"}
)
assert r.ok
print(r.json())from felt_python import refresh_url_layer
refresh_url_layer(map_id, layer_id)Now go to your map and see if any new earthquakes have occurred!
Authentication — token behavior, rotation, and security.
Errors and rate limits — what failures look like and how to handle them.
Listening to updates using webhooks — react to map changes without polling.
API Reference — every endpoint, generated from the OpenAPI spec.
https://felt.com/api/v2
pip install felt-python{
"id": "CjU1CMJPTAGofjOK3ICf1D",
"type": "map",
"title": "My newly created map",
"url": "https://felt.com/map/My-newly-created-map-CjU1CMJPTAGofjOK3ICf1D",
"public_access": "view_only",
"layers": [],
"created_at": "2024-05-25T15:51:34"
}A layer must have finished uploading successfully before it can be refreshed
The Felt SDK has a number of methods for interacting with maps, depending on how you set up your HTML.
All Felt maps are embedded in iframes, and the SDK can do this for you or can connect to an existing Felt iframe.
Felt map IDs are unique identifiers for Felt maps. They are used to embed maps in iframes, and to connect to existing iframes.
To get the ID of a Felt map, click the Map settings button in the main toolbar, and then you can see the Map ID in the Developers section.
Alternatively, you can look at the URL of the map. For example, the map at https://felt.com/map/Map-title-xPV9BqMuYQxmUraVWy9C89BNA has the ID xPV9BqMuYQxmUraVWy9C89BNA.
Throughout the documentation, we'll use the placeholder FELT_MAP_ID to refer to a Felt map ID.
Felt.embed to create an iframeCreate an HTML page with a container element:
Embed a Felt map in your container element and use the SDK to control it by calling Felt.embed, passing the container element as the first argument:
Felt.embed also takes a third argument with options controlling the embed's UI, initial viewport, and authentication — see .
In some cases, you may want to add a "template" iframe to your page. This can be useful if you want to style your iframe in a specific way, or if you already have one map embedded and want to mount and control a different map.
In this case, you can call Felt.embed with the iframe element as the first argument:
There may be cases where you already have a Felt map embedded in an iframe, and you want to control it using the SDK. This can be useful if your HTML is server-rendered with the Felt map already embedded.
In this case, you can call Felt.connect with the iframe's window as the first argument:
Note that in this case, you don't need to pass the Felt map ID to Felt.connect, because we are connecting to a map that has already been embedded.
The Felt SDK provides methods to control the visibility of various entities like layers, layer groups, annotation groups, and legend items. These methods are designed to efficiently handle bulk operations.
All visibility methods use a consistent structure that allows both showing and hiding entities in a single call:
{
show?: string[], // IDs of entities to show
hide?: string[] // IDs of entities to hide
}Control visibility of layers using setLayerVisibility:
felt.setLayerVisibility({
show: ["layer-1", "layer-2"],
hide: ["layer-3"]
});Control visibility of layer groups using setLayerGroupVisibility:
felt.setLayerGroupVisibility({
show: ["group-1", "group-2"],
hide: ["group-3"]
});Similarly, control annotation group visibility with setElementGroupVisibility:
felt.setElementGroupVisibility({
show: ["points-group"],
hide: ["lines-group", "polygons-group"]
});Legend items require both a layer ID and an item ID to identify them. Use setLegendItemVisibility:
felt.setLegendItemVisibility({
show: [
{ layerId: "layer-1", id: "item-1" },
{ layerId: "layer-1", id: "item-2" }
],
hide: [
{ layerId: "layer-1", id: "item-3" }
]
});To focus on a single layer by hiding all others, first get all layers and then use their IDs:
When implementing a toggle, you can use empty arrays for the operation you don't need:
Batch operations: Use a single call with multiple IDs rather than making multiple calls:
Omit unused properties: When you only need to show or hide, omit the unused property rather than including it with an empty array:
The attributes block contains information on how attributes will be shown both on the popup and the table. Each attribute definition can contain the following properties:
displayName
Optional. How this attribute will be shown in different parts of the Felt UI.
format
format accepts any valid — common keys include mantissa (decimal places), thousandSeparated, average (compact notation like 1.2M), output ("percent", "currency"), and prefix/postfix. The object is validated by numbro itself, so anything numbro accepts is valid.
Felt adds one extra key on top of numbro: unit, for displaying measurements. Supported values: meter, kilometer, foot, mile, square_meter, hectare, square_kilometer, square_foot, acre, square_mile, and the adaptive auto-distance and
Most of the properties used on the paint and label blocks can be defined using interpolators to enable zoom-based styling — the property tables on those pages mark which ones accept an Interpolator.
We support multiple types of interpolators: Step functions, linear, exponential and cubic bezier to enable your map looking like you want at each zoom level. See the Interpolators page.
An example of a layer changing feature colors depending on the zoom level can be found below
"paint": {
"color": {"linear": [[14, "red"], [20, "blue"]]}
}On zoom levels lower than 14, features of this layer will be rendered in red color. On zoom levels higher than 20, features of this layer will be rendered in blue color.
In zooms between 14 and 20, color will be linearly interpolated between red and blue.
Style validation errors surface as a banner in the in-app style editor, and as 422 responses with the message in the error detail when styling via the REST API. The JS SDK rejects the setLayerStyle promise with the validation message.
Problem: One of the values set in the style has an unsupported value or an invalid type.
Solution: Change the value to be valid.
Error messages:
Attribute 'displayName' on a legend item of type simple must be a string.
Attribute attribute_name is not a number.
Attribute attribute_name is not a string.
Attribute 'lineCap' is not a supported value. Supported values are butt, round, square.
Attribute 'lineJoin' is not a supported value. Supported values are bevel, round, miter.
Visualization dashArray has to be an array with even length.
Attribute 'offset' must be either an array of numbers or a number.
Attribute 'placement' contains a not supported value. Supported values are N, NE, E, SE, S, SW, W, NW, Center.
Attribute 'placement' contains a not supported value. Supported values are Above, Center, Below.
All values in 'labelAttribute' must be a string.
Visualization 'type' definition must be one of simple, categorical, numeric, heatmap, hillshade, h3.
Attribute 'showOther' must be one of above, below. (Despite the wording, this message refers to the otherOrder field — showOther itself is a boolean, while otherOrder must be "above" or "below".)
Problem: The style defines a categorical visualization, but the maps are not showing the layer
Error messages:
Categories required. A categories array must be defined in the config block when defining a categorical visualization. Read more about categorical visualizations .
Not enough or too many attribute_name values. When defining a categorical visualization, all style and label properties must be an array with either a single value that will apply to all categories or an array with as many values as categories defined in the config block. Read more about categorical visualizations .
The type field chooses how a layer's data is turned into a picture. Pick the type that matches your data and message:
One uniform style for every feature; raster imagery as-is
Each page lists the geometry types it applies to, the properties that matter, and worked examples. For the values you'll plug into these styles, see , , and .
, , and (numeric and tinted hillshade) visualizations all turn a range of values into discrete classes (or a smooth gradient) using the steps field in the config block. The classification method decides where the breaks between classes fall.
It's common to have data update on a regular basis, such as every week or every month. Instead of having to re-upload and style the new data, it can be very convenient to simply refresh a layer using a new data source.
Just like , refreshing a layer with a new file is a two-step process:
Perform a POST request to receive an S3 presigned URL which you can later upload your files to:
Similar to , refreshing an existing URL layer is just a matter of making a single POST
The Felt SDK allows you to control your Felt maps and build powerful, interactive custom applications. You can control many aspects of the Felt UI and map contents, as well as receive notifications of events happening in the map such as clicks, selections, and more.
This feature is available to customers on the . Reach out to .
See our page to explore what you can build with the SDK.
There are two main ways to use the Felt SDK:
Extensions
Embedded maps
Almost every error returned by the Felt API uses the same JSON envelope: an errors array where each entry has a title, a human-readable detail, usually a stable code, and — where applicable — a source telling you which header, parameter, or body field caused the problem.
Treat code as best-effort rather than guaranteed: a few responses — notably field-validation failures and plan-limit errors — carry only title and detail. Branch on the HTTP status first, and use code
The Felt SDK allows you to filter which features are visible in a layer using expressions that evaluate against feature properties. Filters can come from different sources and are combined to determine what's visible.
Note that filters only work on layers that have been uploaded to and processed by Felt — layers created at runtime with cannot be filtered.
By understanding how filters work and combine, you can create dynamic views of your data that respond to user interactions and application state.
Layer filters can come from multiple sources, which are combined to create the final filter:
Style filters: Set by the map creator in the Felt UI
A great way of building data-driven apps using Felt is by triggering a workflow whenever something changes on a map, like someone drawing a polygon around an area of interest or updating the details on a pin.
Instead of polling by listing annotations, comments or data layers on a fixed interval, set up a webhook and Felt will send your endpoint a notification any time a map is updated. This allows you to build integrations on top, such as sending a Slack message or recalculating statistics for a newly-drawn area.
Each webhook is attached to a single map. Whenever the map changes — annotations drawn or edited, layers updated, comments added or resolved — Felt sends a POST request with a JSON body to your webhook URL:
A few things to know about the delivery model:
Event types. map:update
A layer's style is defined in a JSON-based format called , or FSL for short. Editors can view the current style of a layer inside a Felt map by clicking on Actions > Edit style language in a layer's overflow menu (three dots).
Here is an example of a simple visualization, expressed in FSL:
A layer's FSL can be retrieved by performing a simple GET request to a layer's endpoint — the response's style field contains the current style object:
To update a layer's style, we can send a POST
A style in its most basic form contains a version definition but it can be extended to define how we want geometry and labels to show on the map, how the legend should look like, what information is shown in popups and the formatting used when displaying feature properties.
The table below describes which properties can be used in the style definition.
Heatmaps are used to visualize the density of points on a map.
Heatmaps work on point layers only, and show concentration rather than individual feature values — if you need to read a specific attribute, use a visualization instead. The config block must be empty ({}), and heatmaps support no popups or labels.
Heatmap visualizations are defined using "type": "heatmap" and allow the following properties to be set:
A compact, one-per-type overview. For fuller, captioned examples — line casing, proportional symbols, icon-per-category, H3 aggregation popups, raster algebra — see the individual pages.
The popup block contains information on how the popup is displayed and which attributes to show.
These are the fields that each popup block can contain:
Points, lines, polygons, raster
Color (or icon) by a discrete category
Points, lines, polygons, raster
Vary color or size by a numeric value (choropleths, proportional symbols)
Points, lines, polygons, raster
Point density as a smooth surface
Points
Aggregate points into hexagonal bins
Points
Imagery, single/multiband numeric, categorical, and hillshade
Raster






auto-areaOptional. A numbro object that encodes how numeric fields should be shown.
A layer must have finished uploading successfully (status of completed) before it can be refreshed — see Monitoring progress
import requests
# Your API token should look like this:
# api_token = "felt_pat_ABCDEFUDQPAGGNBmX40YNhkCRvvLI3f8/BCwD/g8"
api_token = "<YOUR_API_TOKEN>"
map_id = "<YOUR_MAP_ID>"
layer_id = "<YOUR_LAYER_ID>"
r = requests.post(
f"https://felt.com/api/v2/maps/{map_id}/layers/{layer_id}/refresh",
headers={"Authorization": f"Bearer {api_token}"}
)
assert r.ok
presigned_upload = r.json()import os
from felt_python import refresh_file_layer
# Setting your API token as an env variable can save
# you from repeating it in every function call
os.environ["FELT_API_TOKEN"] = "<YOUR_API_TOKEN>"
map_id = "<YOUR_MAP_ID>"
layer_id = "<YOUR_LAYER_ID>"
new_file_name = "<PATH_TO_NEW_FILE>"
refresh_file_layer(
map_id=map_id,
layer_id=layer_id,
file_name=new_file_name
)Smooth gradient, no discrete classes
Proportional symbols, smooth color ramps
jenks
Natural breaks — minimizes variance within each class
Most cases; a good starting point
quantiles
Equal number of features in each class
Balanced representation, evenly distributed data
equal-intervals
Equal-sized value bins
When the value ranges themselves matter
stddev
Classes in 1σ steps with the middle class centered on the mean
Data with a near-normal distribution
geo-intervals
Break points in a geometric progression between min and max
Multiplicative, strongly skewed data (values must be non-negative)
All shortcut methods except continuous require a count. Raster algebra visualizations support only continuous and equal-intervals.
There are two ways to define steps.
Shortcut (recommended when you don't know the data's distribution). Name a method and a class count and let Felt compute the break points from the data:
For a smooth gradient, use the continuous shortcut:
Explicit break points (when you know the values you want). Provide the break points directly:
Classed: N break points produce N − 1 classes. An explicit color or size array must therefore have exactly N − 1 entries. (A palette shortcut such as @galaxy is expanded for you.)
Continuous: provide 2 or more colors and Felt interpolates between them across the full value range. For proportional symbols, give size: [min, max] and sizes scale smoothly between those two values.
See Numeric visualizations for full color and size examples, and Colors & palettes for palette choices.
continuous
Ephemeral filters: Set temporarily through the SDK
You can inspect these different filter sources using getLayerFilters:
Use setLayerFilters to apply ephemeral filters to a layer:
This replaces any ephemeral filters currently set on the layer. You can also pass an optional note — a message shown on the layer legend while the filter is applied, along with a reset button that lets the user clear it:
The following operators are available:
Comparison: lt (less than), gt (greater than), le (less than or equal), ge (greater than or equal), eq (equal), ne (not equal)
Text: cn (contains), nc (does not contain)
Boolean: and, or
Lookup: in (contained in list), ni (not contained in list)
Null checks: is / isnt (match against null)
See the filters block for more details on filter operators.
You can combine multiple conditions using boolean operators:
Here's a common use case where we filter a layer to show only features that match a property of a selected feature:
Clear filters: Set filters to null to remove them entirely:
Check existing filters: Remember that your ephemeral filters combine with existing style and component filters:
Type safety: Use TypeScript to ensure your filter expressions are valid:
Optional. The attribute (or attributes) used to title the popup if available.
imageAttribute
Optional. The attribute that will be used to populate the popup image if available.
popupLayout
Optional. One of "table" or "list". The way the popup shows its contents. Defaults to "table".
popupLocation
Optional. Where the popup appears: "onMap", "leftSidebar", "rightSidebar", or "modal".
headerLayout
Optional. "standard", "compact", or "none".
keyAttributes
Optional. A list of attributes to show in the popup following the order defined here. If it’s not defined, only attributes with a value will show. If it’s defined, all listed attributes show even when the selected feature doesn’t include them.
url
Iframe popups only. The URL to embed; supports the {{column_name}} template syntax.
width
iframe and html popups only. The popup width in pixels.
height
iframe and html popups only. The popup height in pixels.
An iframe popup embeds external content. Use the template syntax {{column_name}} (or {{['Column Name']}} for names with spaces) to inject feature values into the URL or HTML.
An html popup renders a rich HTML template that is authored and stored with the layer in the Felt app — the FSL block only sets "type": "html" plus the common fields above; the template itself is not part of the style.
H3 hexes show aggregated values rather than raw feature attributes, so their popups reference special attribute names:
"felt:cluster_size" — number of features in the hex
"felt:sum:column", "felt:mean:column", "felt:min:column", "felt:max:column" — aggregates of column
type
Optional. "attributes" (default), "iframe", or "html".
titleAttribute
<html>
<body>
<h1>My Felt app</h1>
<div id="container"></div>
</body>
</html>import { Felt } from "@feltmaps/js-sdk";
const felt = await Felt.embed(
document.querySelector("#container"),
"FELT_MAP_ID",
);
// Now use the SDK
const layers = await felt.getLayers();
const elements = await felt.getElements();
// You also have a reference to the iframe itself:
felt.iframe.style.width = "50%";<html>
<body>
<h1>My Felt app</h1>
<iframe id="my-iframe"></iframe>
</body>
</html>import { Felt } from "@feltmaps/js-sdk";
const felt = await Felt.embed(
document.querySelector("#my-iframe"),
"FELT_MAP_ID",
);<html>
<body>
<h1>My Felt app</h1>
<iframe src="https://felt.com/map/Map-title-xPV9BqMuYQxmUraVWy9C89BNA" id="my-iframe"></iframe>
</body>
</html>import { Felt } from "@feltmaps/js-sdk";
const felt = await Felt.connect(
document.querySelector("#my-iframe").contentWindow
);const layers = await felt.getLayers();
const targetLayerId = "important-layer";
felt.setLayerVisibility({
show: [targetLayerId],
hide: layers
.map(layer => layer?.id)
.filter(id => id && id !== targetLayerId)
});function toggleLayer(layerId: string, visible: boolean) {
felt.setLayerVisibility({
show: visible ? [layerId] : [],
hide: visible ? [] : [layerId]
});
}// Better approach
felt.setLayerVisibility({
show: ["layer-1", "layer-2"],
hide: ["layer-3", "layer-4"]
});
// Less efficient approach
felt.setLayerVisibility({ show: ["layer-1"] });
felt.setLayerVisibility({ show: ["layer-2"] });
felt.setLayerVisibility({ hide: ["layer-3"] });
felt.setLayerVisibility({ hide: ["layer-4"] });// Do this
felt.setLayerVisibility({
show: ["layer-1"]
});"attributes": {
"faa": {
"displayName": "FAA Code",
"format": {
"mantissa": 0,
"thousandSeparated": true
}
},
"wikipedia": {"displayName": "Wikipedia"}
}"attributes": {
"parcel_area": {
"displayName": "Parcel area",
"format": {"mantissa": 1, "unit": "auto-area"}
}
}# Your API token and map ID should look like this:
# FELT_API_TOKEN="felt_pat_ABCDEFUDQPAGGNBmX40YNhkCRvvLI3f8/BCwD/g8"
FELT_API_TOKEN="<YOUR_API_TOKEN>"
MAP_ID="<YOUR_MAP_ID>"
LAYER_ID="<YOUR_LAYER_ID>"
curl \
-X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${FELT_API_TOKEN}" \
"https://felt.com/api/v2/maps/${MAP_ID}/layers/${LAYER_ID}/refresh"import requests
# Your API token and map ID should look like this:
# api_token = "felt_pat_ABCDEFUDQPAGGNBmX40YNhkCRvvLI3f8/BCwD/g8"
api_token = "<YOUR_API_TOKEN>"
map_id = "<YOUR_MAP_ID>"
layer_id = "<YOUR_LAYER_ID>"
r = requests.post(
f"https://felt.com/api/v2/maps/{map_id}/layers/{layer_id}/refresh",
headers={"Authorization": f"Bearer {api_token}"}
)
assert r.ok
print(r.json())from felt_python import refresh_url_layer
refresh_url_layer(map_id, layer_id)# This code is a continuation of the previous Python code block
# and assumes you already have a "presigned_upload" variable
file_name = "<YOUR_FILE_WITH_EXTENSION>" # Example: regions.geojson
url = presigned_upload["url"]
presigned_attributes = presigned_upload["presigned_attributes"]
# A 204 response indicates that the upload was successful
with open(file_name, "rb") as file_obj:
output = requests.post(
url,
# Order is important, file should come at the end
files={**presigned_attributes, "file": file_obj},
)# Nothing! Refreshing a file layer is a single step with the felt-python library{"version": "2.3.1", "type": "simple", "config": {}, "paint": {}, "label": {}}{
"version": "2.3.1",
"type": "simple",
"config": {"labelAttribute": ["name"]},
"paint": {"color": "#8F7EBF", "size": 4, "strokeColor": "auto", "strokeWidth": 1},
"label": {"color": "auto", "haloColor": "auto", "placement": ["E"], "offset": [6, 0]}
}{
"version": "2.3.1",
"type": "simple",
"config": {"labelAttribute": ["river_name"]},
"paint": {"color": "hsl(217, 80%, 40%)", "size": 2},
"label": {"color": "auto", "fontStyle": "italic", "placement": "Above", "repeatDistance": 200}
}{
"version": "2.3.1",
"type": "simple",
"config": {"labelAttribute": ["name"]},
"paint": {"color": "#3B82F6", "strokeColor": "auto", "strokeWidth": 1, "opacity": 0.8},
"label": {"color": "auto", "haloColor": "auto", "placement": ["Center"]}
}{
"version": "2.3.1",
"type": "categorical",
"config": {
"categoricalAttribute": "primary_fuel",
"categories": {"type": "top", "count": 6},
"showOther": true
},
"paint": {"color": "@catPalette1", "size": 4, "strokeColor": "auto", "strokeWidth": 1},
"legend": {"displayName": "auto"}
}{
"version": "2.3.1",
"type": "numeric",
"config": {"numericAttribute": "Renter occupied (%)", "steps": {"type": "jenks", "count": 5}},
"paint": {"color": "@galaxy", "opacity": 0.9, "strokeColor": "auto", "strokeWidth": 0.5},
"legend": {"displayName": "auto"}
}{
"version": "2.3.1",
"type": "heatmap",
"config": {},
"paint": {"color": "@purpYlPink", "size": 10, "intensity": 0.2, "opacity": 0.9},
"legend": {"displayName": {"0": "Low", "1": "High"}}
}{
"version": "2.3.1",
"type": "h3",
"config": {
"aggregation": "sum",
"numericAttribute": "capacity_mw",
"baseBinLevel": 3,
"binMode": "fixed",
"steps": {"type": "quantiles", "count": 5}
},
"paint": {"color": "@riverine"},
"legend": {"displayName": "auto"}
}{
"version": "2.3.1",
"type": "numeric",
"config": {"band": 1, "steps": {"type": "continuous"}, "noData": [-9999], "rasterResampling": "linear"},
"paint": {"color": "@mplVirdis", "opacity": 1},
"legend": {"displayName": "auto"}
}{
"config": {
"numericAttribute": "population_density",
"steps": {"type": "jenks", "count": 5}
}
}{
"config": {
"numericAttribute": "temperature",
"steps": {"type": "continuous"}
}
}{
"config": {
"numericAttribute": "magnitude",
"steps": [0, 3, 5, 7, 9]
}
}const filters = await felt.getLayerFilters("layer-1");
console.log(filters.style); // Base filters from the layer style
console.log(filters.components); // Filters from legend components
console.log(filters.ephemeral); // Filters set through the SDK
console.log(filters.combined); // The final result of combining all filtersfelt.setLayerFilters({
layerId: "layer-1",
filters: ["POPULATION", "gt", 1000000]
});felt.setLayerFilters({
layerId: "layer-1",
filters: ["POPULATION", "gt", 1000000],
note: "Showing cities with over 1M residents",
});felt.setLayerFilters({
layerId: "layer-1",
filters: [
["POPULATION", "gt", 1000000],
"and",
["COUNTRY", "eq", "USA"]
]
});// Listen for selection changes
felt.onSelectionChange({
handler: async ({ selection }) => {
// Find the first selected feature
const selectedFeature = selection.find(node => node.type === "feature");
if (selectedFeature) {
// Get the state code from the selected feature
const stateCode = selectedFeature.entity.properties.STATE_CODE;
// Filter the counties layer to show only counties in the selected state
felt.setLayerFilters({
layerId: "counties-layer",
filters: ["STATE_CODE", "eq", stateCode]
});
} else {
// Clear the filter when nothing is selected
felt.setLayerFilters({
layerId: "counties-layer",
filters: null
});
}
}
});felt.setLayerFilters({
layerId: "layer-1",
filters: null
});const filters = await felt.getLayerFilters("layer-1");
// Check if there are any style filters before adding ephemeral ones
if (filters.style) {
console.log("This layer already has style filters");
}import type { Filters } from "@feltmaps/js-sdk";
const filter: Filters = ["POPULATION", "gt", 1000000];
felt.setLayerFilters({
layerId: "layer-1",
filters: filter
});"popup": {
"titleAttribute": "name",
"keyAttributes": ["osm_id", "highway", "ref", "place"],
"popupLayout": "list",
"popupLocation": "onMap",
"headerLayout": "standard"
}"popup": {
"type": "iframe",
"url": "https://example.com/details/{{id}}",
"popupLocation": "modal",
"width": 600,
"height": 800
}"popup": {
"titleAttribute": "felt:sum:revenue",
"keyAttributes": ["felt:cluster_size", "felt:mean:revenue", "felt:max:revenue"]
}When creating an extension, you automatically have access to a FeltController object named felt with no setup required. This controller provides all the methods you need to interact with your Felt map, including getViewport, createElement, setLayerStyle, and many more.
Embed Felt maps in your own applications and control them remotely.
A Felt map to embed. Open any map you have access to and grab its ID from the URL: in felt.com/map/Readable-Title-xPV9BqMuYQxmUraVWy9C89BNA, the ID is the trailing part, xPV9BqMuYQxmUraVWy9C89BNA.
The map's sharing settings must allow the visitor to view it. Public and unlisted maps work out of the box; for private maps see Embed options.
Install the SDK using your preferred package manager:
Alternatively, load it straight from a CDN in a <script type="module"> — no build step required:
Create an HTML page with a container element. Give the container an explicit height — an iframe inside a zero-height container renders as an invisible sliver, which is the most common first-run problem:
Embed a Felt map in your container element and use the SDK to read from it:
You should see your map load inside the container, with the Felt legend and zoom controls visible. Open the browser console and you should see the layer count logged. If the container stays empty, check that the map ID is correct and that the map's sharing settings allow viewing.
Throughout these docs, code examples assume a controller variable named felt, whether it came from Felt.embed or from the extension environment.
General concepts — the mental model: controllers, promises, listeners, and what persists.
Controlling maps — viewports, and connecting to existing iframes.
Embed options — UI controls, initial viewport, private maps.
Integrating with React — React hooks for the SDK, and the React starter repo for a quick start.
// In a Felt extension, the controller is automatically available
const layers = await felt.getLayers();
// Listen for map events
felt.onSelectionChange({
handler: ({ selection }) => console.log("Selection changed:", selection),
});npm install @feltmaps/js-sdkimport { Felt } from "https://esm.run/@feltmaps/js-sdk";<html>
<head>
<style>
#container { height: 500px; }
</style>
</head>
<body>
<div id="container"></div>
<script type="module" src="main.js"></script>
</body>
</html>// main.js
import { Felt } from "@feltmaps/js-sdk";
const felt = await Felt.embed(
document.querySelector("#container"),
"FELT_MAP_ID", // Replace with your map's ID
);
const layers = await felt.getLayers();
console.log(`This map has ${layers.length} layers`);401
unauthorized, invalid_access_token
Missing, malformed, revoked, or wrong-workspace token. Requests with a valid token for a resource in a different workspace also return 401. Permission failures also return 401, not 403 — for example, editing or deleting a map your account can only view.
Check the Authorization: Bearer header and that the token was created in the same workspace as the resource. If the token is fine, ask a workspace admin for the required role on the resource. See .
When handling responses in code, print the error body rather than only asserting success — detail almost always tells you exactly what's wrong:
Two independent limits apply:
Per-IP request throttle — currently 300 requests per minute per IP address. Exceeding it returns 429 with the too_many_requests code above. Spread bulk work out or batch it (for example, upsert many annotations in one POST /elements call instead of one call per feature).
Plan usage limits — depending on your plan, API usage may also be subject to an overall usage limit. Every API response includes an x-api-limit-exceeded header (true or false); if your workspace exceeds its limit, requests return 429. Reach out to if you have questions about your plan's API access.
When you receive a 429, retry with exponential backoff and jitter rather than immediately — and check x-api-limit-exceeded to distinguish the per-IP throttle (false) from a plan usage limit (true).
List endpoints (GET /projects, GET /library, GET /maps/{map_id}/elements, and so on) currently return all results in a single response — there are no pagination parameters. For very large maps, prefer scoping your reads (for example, listing a single element group) over repeatedly fetching full collections.
The current API version is v2, served under https://felt.com/api/v2. The machine-readable OpenAPI spec for the exact version in production is always available at:
{
"errors": [
{
"title": "Not found",
"detail": "Map not found",
"code": "not_found",
"source": { "parameter": "map_id" }
}
]
}r = requests.post(url, headers=headers, json=body)
if not r.ok:
print(r.status_code, r.json()["errors"])
r.raise_for_status()GET https://felt.com/api/v2/openapi.jsonDeliveries are coalesced. A rapid burst of edits to the same map may result in a single notification rather than one per edit. Treat each delivery as "this map changed, re-read it", not as a per-edit event stream.
Retries. If your endpoint does not respond with a 2xx status, delivery is retried up to 10 times with exponential backoff. This means delivery is at least once: your handler should be idempotent. Redirects are not followed.
Creation is UI-only. Webhooks are created in workspace settings (see below); there are no REST endpoints for managing them.
Every webhook has a signing key, shown in the workspace settings UI when you create it. Felt signs each delivery with a felt-signature HTTP header containing the Base64-encoded HMAC-SHA256 of the raw JSON body, computed with that signing key.
Verify the signature before acting on a payload — it proves the request came from Felt and not from a third party who discovered your URL:
import base64
import hashlib
import hmac
def verify_felt_signature(signing_key: str, raw_body: bytes, signature_header: str) -> bool:
expected = base64.b64encode(
hmac.new(signing_key.encode(), raw_body, hashlib.sha256).digest()
).decode()
return hmac.compare_digest(expected, signature_header)import { createHmac, timingSafeEqual } from "node:crypto";
function verifyFeltSignature(signingKey, rawBody, signatureHeader) {
const expected = createHmac("sha256", signingKey).update(rawBody).digest("base64");
return timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}Compute the HMAC over the raw request body exactly as received — re-serializing the parsed JSON can change key order or whitespace and produce a different signature.
Two things are needed in order to make use of webhooks:
A Felt map which will serve as the basis for the webhook. Updates will be sent whenever something on this map changes.
A webhook URL where the updates will be sent in the form of POST requests.
Workspace admins and editors can set up webhooks in the Developers tab of the Workspace Settings page.
Simply click on Create a new webhook, select a map to listen to changes and paste in a webhook URL where the updates will be sent to.
In order to use webhooks effectively, a receiving service must be set up to trigger actions based on the updates sent by the Felt API. Here are some examples of how to set up a webhook using Felt and an external service.
{
"attributes": {
"type": "map:update",
"map_id": "Jzjr8gMKSrCOxZ1OSMT49CB",
"updated_at": "2024-04-29T12:16:46"
}
}/update_stylecurl \
-X POST \
"https://felt.com/api/v2/maps/${MAP_ID}/layers/${LAYER_ID}/update_style" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${FELT_API_TOKEN}" \
--data '{"style": {"paint": {"color": "green", "opacity": 0.9, "size": 30, "strokeColor": "auto", "strokeWidth": 1}, "legend": {}, "type": "simple", "version": "2.3.1"}}'new_fsl = {
"paint": {
"color": "green",
"opacity": 0.9,
"size": 30,
"strokeColor": "auto",
"strokeWidth": 1
},
"legend": {},
"type": "simple",
"version": "2.3.1"
}
r = requests.post(
f"https://felt.com/api/v2/maps/{map_id}/layers/{layer_id}/update_style",
json={"style": new_fsl},
headers={"Authorization": f"Bearer {api_token}"}
)
assert r.ok
print(r.json())from felt_python import update_layer_style
new_fsl = {
"paint": {
"color": "green",
"opacity": 0.9,
"size": 30,
"strokeColor": "auto",
"strokeWidth": 1
},
"legend": {},
"type": "simple",
"version": "2.3.1"
}
update_layer_style(
map_id=map_id,
layer_id=layer_id,
style=new_fsl,
)You can find examples of FSL for different visualization types in the Felt Style Language section of these docs:
Simple visualizations: same color and size for all features (vector) or pixels (raster).
Categorical visualizations: different color per feature or pixel, based on a categorical attribute
Numeric visualizations: different color or size per feature or pixel, based on a numeric attribute.
Heatmaps: a density-based visualization style, for vector point layers.
: aggregate points into hexagonal bins.
: imagery, single/multiband numeric, categorical, and hillshade styling for raster layers.
{
"config": {"labelAttribute": ["type"]},
"legend": {},
"paint": {
"color": "blue",
"opacity": 0.9,
"size": 30,
"strokeColor": "auto",
"strokeWidth": 1
},
"type": "simple",
"version": "2.3.1"
}# Your API token should look like this:
# FELT_API_TOKEN="felt_pat_ABCDEFUDQPAGGNBmX40YNhkCRvvLI3f8/BCwD/g8"
FELT_API_TOKEN="<YOUR_API_TOKEN>"
MAP_ID="<YOUR_MAP_ID>"
LAYER_ID="<YOUR_LAYER_ID>"
curl \
-H "Authorization: Bearer ${FELT_API_TOKEN}" \
"https://felt.com/api/v2/maps/${MAP_ID}/layers/${LAYER_ID}"import requests
# Your API token should look like this:
# api_token = "felt_pat_ABCDEFUDQPAGGNBmX40YNhkCRvvLI3f8/BCwD/g8"
api_token = "<YOUR_API_TOKEN>"
map_id = "<YOUR_MAP_ID>"
layer_id = "<YOUR_LAYER_ID>"
r = requests.get(
f"https://felt.com/api/v2/maps/{map_id}/layers/{layer_id}",
headers={"Authorization": f"Bearer {api_token}"}
)
assert r.ok
print(r.json())import os
from felt_python import get_layer_details
# Setting your API token as an env variable can save
# you from repeating it in every function call
os.environ["FELT_API_TOKEN"] = "<YOUR_API_TOKEN>"
map_id = "<YOUR_MAP_ID>"
layer_id = "<YOUR_LAYER_ID>"
layer_details = get_layer_details(map_id, layer_id)
current_style = layer_details["style"]type
Optional. One of , , , , , or (for raster) . Defaults to simple.
config
Optional. A block that contains some configuration options to be used across the style. .
paint
Optional. An object (or array of objects) that defines how the data will be drawn.
label
Optional. An object that defines how the labels will be drawn.
legend
Optional. Defines how this layer will be shown on the layer panel.
popup
Optional. Defines how the popup is shown and what’s included.
attributes
Optional. Defines how attributes are shown both in the popup and the table.
filters
Optional. A data filter definition that defines which data will be rendered.
All of these keys are siblings at the top level of the object — none are nested inside another:
version
Mandatory. Defines which version this style adheres to. The current version is "2.3.1".
{
"version": "2.3.1",
"type": "simple",
"config": {},
"paint": {},
"label": {},
"legend": {},
"popup": {}
}color
string
"@geyser"
—
A name, from less density to more.
size
number
10
1–30 px
Radius of influence in pixels. Larger = smoother, smaller = more detailed.
Tune size for the zoom you expect to view at — larger at low zoom, smaller at high zoom.
This is an example of a heatmap visualization
defined with the following visualization
{
"version": "2.3.1",
"type": "heatmap",
"config": {},
"legend": {"displayName": {"0": "Low", "1": "High"}},
"paint": {"color": "@purpYlPink", "size": 10, "intensity": 0.2}
}The Felt SDK allows you to add GeoJSON data to your maps from various sources:
Remote URLs
Local files
Programmatically generated GeoJSON data
GeoJSON layers created via the SDK are temporary and session-specific - they're not permanently added to the map and won't be visible to other users.
When creating a GeoJSON layer, you can specify different styles for each geometry type (Point, Line, Polygon) that might be found in the source. Each geometry type will create its own layer. It's important to note that GeoJSON layers added via the SDK have limited capabilities compared to regular Felt layers - they cannot be filtered, nor can statistics be fetched for them.
Use the method to add GeoJSON layers to your map. This method accepts different source types depending on where your GeoJSON data comes from.
To create a layer from a GeoJSON file at a remote URL:
To create a layer from a GeoJSON file on the user's device:
To create a layer from GeoJSON data that you've generated or processed in your application. This approach is useful when you need to dynamically generate GeoJSON data based on user interactions or other app states:
When creating GeoJSON layers, you can specify different styles for each geometry type that might be found in your data. The SDK will create separate layers for each geometry type:
Each style should be a valid (Felt Style Language) style. If you don't specify styles, Felt will apply default styles based on the geometry type.
To remove a GeoJSON layer:
Note that this only works for layers created via the SDK's createLayersFromGeoJson method, not for layers added through the Felt UI.
For GeoJSON layers created from URLs, you can set automatic refreshing:
At Creation Time: By setting the refreshInterval parameter when creating the layer. The refreshInterval parameter is optional and specifies how frequently (in milliseconds) the layer should be automatically refreshed from the URL. Valid values range from 250ms to 5 minutes (300,000ms). If set to null or omitted, the layer won't refresh automatically.
Manual refresh: Replace the source property of any layer you have created, using the method to update the source data:
Explore examples of what you can build with the Felt SDK. These examples showcase different approaches to creating interactive map experiences - from extensions that run directly within Felt to embedded maps in custom applications.
Extensions run directly within Felt maps, giving you immediate access to all SDK functionality. Here are some examples built using AI assistance:
Visualize transportation patterns by drawing lines to destination counties on click. Features travel mode options and filtering capabilities to analyze commuting data across different regions. View the map here.
Compare neighborhoods side-by-side with automated analysis of land use patterns. This tool helps users understand demographic and geographic differences between areas. View the map here.
Bring geographic data to life with animations showing the flow of the Mississippi River Basin from headwaters to the Gulf of Mexico, demonstrating how to create compelling temporal visualizations. View the map here.
Guide users through agricultural regions with an interactive narrative experience that combines storytelling with geographic exploration. View the map .
Embed Felt maps in your own applications and control them with the SDK. Here are some examples built with React and hosted on CodeSandbox:
This interactive application leverages the Tool API to enable users to draw custom geometries that retrieve filtered GeoJSON data from an ESRI FeatureService. The application creates a dynamically styled GeoJSON layer to visualize solar potential data, helping users identify optimal locations for solar installations. View the app and code .
Create powerful business intelligence tools by combining Felt's layer statistics with popular charting libraries. This example demonstrates how to build interactive visualizations that leverage layer filters. View the app and code .
Enhance map usability with a custom legend that extracts and uses styling information to generate SVG icons. The code demonstrates how to build a nested folder structure with visibility toggles using layer filters, providing a pattern for organizing complex data layers. View the app and code .
Build comprehensive multi-view dashboards by embedding multiple Felt maps on a single page. This example demonstrates how to create synchronized map views that communicate with each other, enabling users to simultaneously view different geographic contexts or zoom levels of the same data. View the app and code .
Create engaging interactive experiences with customizable map lenses that reveal different data layers or styling as users explore. This technique allows for compelling before/after comparisons or the ability to highlight specific data attributes within a defined area. View the app and code .
Provides a pattern for integrating third-party geospatial APIs with Felt maps. This example demonstrates how to make API requests based on annotation geometry and map interactions, process the returned data, and visualize isochrones as dynamic layers. View the app and code .
Felt.embed() takes an optional third argument that controls how the embedded map looks and behaves:
const felt = await Felt.embed(container, FELT_MAP_ID, {
uiControls: {
showLegend: false,
cooperativeGestures: false,
},
initialViewport: {
center: { latitude: 40.7128, longitude: -74.006 },
zoom: 10,
},
});All uiControls options are booleans:
You can change these after the map has loaded with :
initialViewport overrides the map's saved viewport with a center and zoom for this embed:
Public and unlisted maps embed with no extra configuration. To embed a private map for visitors who aren't logged into Felt, generate a short-lived embed token server-side with the REST API and pass it as the token option:
Your server calls , using your . The visitor's user_email is a required query parameter, not a body field, and that address should belong to a member of your workspace. The response contains a token valid for 15 minutes.
Your page passes that token to Felt.embed:
Never call the embed token endpoint from the browser — that would expose your API token. Generate embed tokens on your server and hand only the short-lived token to the client.
The filters block contains information on how the layer is being filtered before displaying. In order for a feature to be shown on the map it must evaluate the filter expression to true.
Filters are written using a JSON infix notation that looks like one of [identifier, operator, operand], true or false .
Valid identifiers are either a feature property or a nested expression.
Valid operators are:
"lt" – Less than
"gt" – Greater than
"le" – Less than or equal to
Operands are:
A numerical value, a string value, a boolean value
An array of numerical, string, or boolean values, a shorthand expanded to these patterns:
Input 1: [id, "in", [element1, …, elementN]]
In cases of type mismatch cast the identifier value to the operand’s type
Type casting applies element-wise to lists with "in" and "ni" operators
Case & diacritics: eq, ne, gt, ge, lt, le, cn, and nc compare strings case-insensitively and diacritic-insensitively. ["status", "eq", "active"] matches "Active"
There is no single "between" operator — combine two comparisons:
Check that a value exists and is non-empty:
Three or more conditions must be written as nested pairs, not a flat list:
Adding a legend block to a visualization makes a legend entry appear for this visualization.
Each legend entry is shown with the geometry type and color defined by the dataset and the visualization block.
While simple visualizations will generate a single legend entry, categorical visualizations will generate a legend entry per category.
legend blockThe only field is displayName. For categorical visualizations it maps each category to its label; for numeric/heatmap visualizations it maps the index of each class ("0", "1", …) to its label. Set "displayName": "auto" to let Felt generate the labels for you — from the computed breaks for numeric classes, or from the category names for categorical visualizations ("auto" requires steps or categories to be set in config). An empty legend: {} still produces a legend entry using the dataset's geometry and color.
For continuous (non-stepped) color-by and heatmap legends keyed by index, "0" labels the lowest value and the highest index labels the highest value. Continuous size legends are the historical exception: their indices run inverted, with "0" labeling the maximum, "1" the midpoint, and "2" the minimum.
The Biodiversity Hotspots layer in Felt has a simple visualization with a legend defined as follows:
The Plant Hardiness Zones layer in Felt has a categorical visualization with a legend defined as follows:
The visual display of numeric legends varies based on the style method (stepped or continuous) and the geometry type (point, line, polygon).
The displayName can be modified in the legend block similar to simple and categorical style types.
Heatmap legends are defined as follows:
Notice that the displayName mapping goes from 0 (left value) to 1 (right value)
This guide covers the mental model and the common patterns used throughout the Felt SDK. Understanding these once makes every other page predictable.
Everything you do with the SDK goes through a controller object — named felt in all the examples in these docs. How you get it depends on where your code runs:
Embeds — your code runs in your page, and the map lives in an iframe. Felt.embed(container, mapId, options) creates the iframe and returns a controller for it. You can also use Felt.connect(iframe.contentWindow) to attach a controller to a Felt iframe you've already placed on the page. See and .
Annotations sit on top of all data layers on a map. They are usually drawn in the Felt app, but can also be created and updated via the API.
Annotations are returned as a .
Returns a list of GeoJSON Feature Collections, one for each annotation group.
Each annotation is represented by a feature in the POST
To work with Felt embeds in React, we have a starter template that you can use as a starting point.
This is available on GitHub in the repository.
In that repo, you will find a feltUtils.ts file that demonstrates some ways to make using the Felt SDK in React easier. The SDK itself is framework-agnostic and ships no React bindings — the hooks below are small wrappers you copy into your own project.
A common use case for building apps on Felt is to be notified when entities are updated. The main example of this is when you want to change the visibility of say a layer, and have your own UI reflect that change.
Rather than keeping track of the visibility of entities yourself, you can use the Felt SDK to listen for changes to the visibility of entities.
Here is an example of how you might do this for layers assuming you already have a reference to a Layer object, e.g. from calling felt.getLayers():
Every listener on the Felt controller follows the same pattern: you pass a handler (and sometimes an options object scoping the listener to a specific entity), and the call returns an unsubscribe function.
Always call the unsubscribe function when your UI unmounts or the listener is no longer needed.
Simple visualizations are those that show each feature in a vector dataset using the same style or the image as it is in raster ones.
Simple visualizations must define "type": "simple" and a single value for each supported style and label properties.
The Airports layer in Felt is an example of a simple visualization using a vector dataset
and is defined by the following style:
Raster layers can also use simple to display imagery as-is — see .
Icon markers (points). Set iconImage to draw points as icons; the icon takes the layer's color
H3 visualization is a way to aggregate point data into a grid of H3 cells.
H3 visualizations are defined using "type": "h3" . They generally share the same properties and behaviors as . Properties of specific relevance to H3 are:
Categorical visualizations use a categorical attribute and the categories within it to apply styling to discrete categories of the attribute. (On raster datasets, categories are raw pixel values instead — see .)
Categorical visualizations are defined using "type": "categorical" and, for every supported style and label property used, either a single value that will apply to all categories or an array of different values for each category.
You can list the categories explicitly, or use a shortcut when you don't know the exact values: {"type": "top", "count": N} keeps the N most common values (vector), and {"type": "all"} covers every unique pixel value (raster). When showOther: true is combined with an explicit categories array, paint/label arrays need
The Felt SDK provides functionality for reading the current selection state and selecting features on the map programmatically. This is useful for building interactive experiences that respond to data analysis or user interactions.
Features are individual data points within layers. You can select features programmatically using the method. Only one feature can be selected at a time - selecting a new feature will replace the current selection.
The method accepts options to control the selection behavior:
showPopup (boolean, default: true) - Whether to display the feature information popup
The Felt SDK provides methods to control the map's viewport (the visible area of the map) and handle user interactions like clicking and hovering on the viewport.
You can get the current viewport state using getViewport():
There are two main ways to set the viewport: moving to a specific point, or fitting to bounds.
Use setViewport() to move the map to a specific location:
Use fitViewportToBounds() to adjust the viewport to show a specific rectangular area:
To stay in sync with viewport changes, use the onViewportMove method:
Interpolators are functions that use the current zoom level to get you a value. The following interpolators are currently supported:
{ "step": [output0, Stops[]] }: Computes discrete results by evaluating a piecewise-constant function defined by stops on a given input. Returns the output value of the stop with a stop input value just less than the input one. If the input value is less than the input of the first stop, output0 is returned.
Stops are defined as pairs of [zoom, value] where zoom is the minimum zoom level where value is returned and value can be number | string | boolean. Note that stops need to be defined by increasing zoom level.
403
forbidden, over_storage_limit, over_processing_limit, unauthorized
Your workspace has hit a plan limit — data hosting or monthly data processing — or the action requires a plan your workspace isn't on.
Read detail: it names the limit. Reach out to our team to raise it.
404
not_found
The resource doesn't exist. The source.parameter field names the offending ID.
Verify the ID. Remember that map IDs come from the map URL, while layer IDs come from API responses.
422
invalid
The request body or parameters failed validation. source.pointer identifies the invalid field.
Fix the field named in detail / source and retry.
429
too_many_requests
You hit a rate limit (see below).
Back off and retry later.

intensity
number
0.5
0.1–3.0
How quickly density saturates. Higher = more contrast.
opacity
number
0.9
0–1
Transparency.











showLegend
true
Whether the legend is shown.
cooperativeGestures
true
Adjusted gesture behavior for embeds: on mobile, one-finger drag scrolls the page while two fingers pan the map; on desktop, scroll-to-zoom requires holding Ctrl/Cmd. Disable for app-like embeds where the map is the main content.
fullScreenButton
true
Shows a button that opens the map in a new tab or window.
geolocation
false
Shows a geolocation button that plots and tracks the visitor's position.
zoomControls
true
Shows the zoom controls (bottom right). Hiding them does not prevent zooming.
scaleBar
true
Shows the scale bar.
"ge" – Greater than or equal to"eq" – Equal to
"ne" – Not equal to
"and" – And, cast to boolean
"or" – Or, cast to boolean
"cn" – Contains the operand, cast to string
"nc" – Does not contain the operand, cast to string
"in" – Contained in the operand list
"ni" – Not contained in the operand list
"is" – Used to match against null values
"isnt" – Used to match against null values
Expansion 1: id is equal ("eq") to one or more of the elements
Input 2: [id, "ni", [element1, …, elementN]]
Expansion 2: id is not equal ("ne") to any of the elements
Not defined for operators other than "in" and "ni"
A nested expression
"ACTIVE"in / ni are case-sensitive, unlike the operators above, and do per-element type coercion (["id", "in", [5]] matches a string "5"). in with an empty array always returns false; ni with an empty array always returns true.
Null handling: use is / isnt only for null/existence checks (with null as the value) — not for value equality. Most other operators yield null (and filter the feature out) when the column is missing or null.
Type coercion: the left-hand value is cast to match the right-hand type, so ["score", "eq", 100] matches whether the column stores 100 or "100".





For each feature, including an existing annotation ID (felt:id) will result in the annotation being updated on the map. If the ID is omitted or does not match an existing annotation, a new annotation is created.
Styling is controlled with felt:-prefixed properties on each feature — for example felt:color, felt:opacity, felt:strokeWidth, felt:strokeStyle, felt:size, and felt:text for text annotations. Properties without the felt: prefix are stored as the annotation's data attributes. Request bodies are limited to 1 MB, and very complex geometries may be simplified on import.
Delete an annotation by its ID. Annotation IDs are returned as the felt:id property when listing annotations.
# Your API token and map ID should look like this:
# FELT_API_TOKEN="felt_pat_ABCDEFUDQPAGGNBmX40YNhkCRvvLI3f8/BCwD/g8"
# MAP_ID="CjU1CMJPTAGofjOK3ICf1D"
FELT_API_TOKEN="<YOUR_API_TOKEN>"
MAP_ID="<YOUR_MAP_ID>"
curl \
-H "Authorization: Bearer ${FELT_API_TOKEN}" \
"https://felt.com/api/v2/maps/${MAP_ID}/elements"import requests
# Your API token and map ID should look like this:
# api_token = "felt_pat_ABCDEFUDQPAGGNBmX40YNhkCRvvLI3f8/BCwD/g8"
api_token = "<YOUR_API_TOKEN>"
map_id = "<YOUR_MAP_ID>"
r = requests.get(
f"https://felt.com/api/v2/maps/{map_id}/elements",
headers={"Authorization": f"Bearer {api_token}"}
)
assert r.ok
print(r.json())Select HTTP / Webhook, then New Requests (Payload Only), and give your newly-created source a name.
Copy the endpoint URL. It will look like https://XXX.m.pipedream.net
In Felt, navigate to the Developers tab of your workspace settings and click on Create new webhook
Paste the endpoint URL from Pipedream into the Webhook URL text field, select your map in the dropdown and click Create.
To test your webhook:
Navigate to the Felt map that's linked to the webhook
Make any change: add a pin, draw with the marker, change the color of a polygon, update sharing permissions...
Back in Pipedream, verify that new events appear for the new source. Note that Pipedream wraps the request in its own envelope — the body field shown by Pipedream is the payload Felt sent:
You may also configure a script to run using the above input.
Advanced Settings, make sure to check Enable function URLSet Auth type to NONE and click on Create function
In the next screen, copy the Function URL. It should look like https://{LAMBDA_ID}.lambda-url.{REGION}.on.aws
Continue configuring your Lambda function as usual by editing the code that will run on map updates
In Felt:
Navigate to the Developers tab of your workspace settings and click on Create new webhook
Paste the function URL from AWS into the Webhook URL text field, select your map in the dropdown and click Create.
Since the function URL uses Auth type: NONE, anyone who discovers the URL can invoke it — make sure your function verifies the felt-signature header before doing any work.

Extensions — your code runs inside the Felt app, and receives a ready-made controller: no embedding required. See the extensions documentation.
The controller communicates with the map via message passing, which is why every read is asynchronous (see promises below). The same controller API is available in both contexts.
A crucial rule for building on the SDK: changes made via the SDK are visible only to the current session — they are not saved to the map. This applies to:
Annotations created with createElement
Layers created with createLayersFromGeoJson
Style changes made with setLayerStyle
Filters set with setLayerFilters
This makes the SDK safe for building per-visitor experiences on shared maps: ten visitors can each see their own filters and drawings without affecting each other or the underlying map. To make persistent changes to a map, use the REST API.
All methods in the Felt SDK are asynchronous and return Promises — writes as well as reads. This means you'll need to use await or .then() when calling them:
The SDK follows a consistent pattern for getting entities. For each entity type, there are usually two getters:
A singular getter for retrieving one entity by ID:
A plural getter that accepts constraints for retrieving multiple entities:
The plural getters also allow you to pass no constraints, in which case they'll return all entities of that type:
Singular getters return null when the entity doesn't exist, and plural getters can contain null entries — always check before using the result:
When you need multiple entities, use the plural methods with constraints rather than making multiple individual calls:
Each entity type has a corresponding change listener method following the pattern on{EntityType}Change:
There are also various other setters and getters in the Felt SDK that follow this convention as much as possible. For example, selection:
And layer filters — note that this particular listener receives the filters value directly, with no wrapper object:
For the full list of listeners and their payloads, see the Events reference.
All change listeners return an unsubscribe function that should be called when you no longer need the listener:
This is particularly important in frameworks like React where you should clean up listeners when components unmount:
Change listeners always take a single object parameter containing both options and handler. This structure makes it easier to add new options in the future without breaking existing code:
When dealing with mixed collections of entities (like in selection events), each entity is wrapped in an EntityNode object that includes type information:
sizeRoad-style casing (lines). A paint array renders last-to-first, so the wider casing layer goes second (underneath) and the narrower fill goes first (on top).

fitViewport (boolean | { maxZoom: number }, default: true) - Whether to fit the viewport to the feature. Can be true, false, or an object with maxZoom to limit the zoom level used.
You can get the current selection state using getSelection(). This returns an array of EntityNode objects, each representing a selected entity. The selection can include various types of entities at the same time, such as annotations and features:
Remove current selections using clearSelection():
To stay in sync with selection changes, use the onSelectionChange() method:
Clean up listeners: Always store and call the unsubscribe function when you no longer need to listen for selection changes:
Handle empty selection: Remember that the selection array might be empty if nothing is selected:
By following these patterns, you can build robust interactions based on what users select in your Felt map.
onPointerClick:Track mouse movement over the map using onPointerMove:
Cleanup: Always store and call unsubscribe functions when you're done listening for events:
Throttling: For pointer move events, consider throttling your handler if you're doing expensive operations:
By using these viewport controls and interaction handlers, you can create rich, interactive experiences with your Felt map.
const layerResult = await felt.createLayersFromGeoJson({
source: {
type: "geoJsonUrl",
url: "https://example.com/data/neighborhoods.geojson",
// Optional: Auto-refresh every 30 seconds
refreshInterval: 30000
},
name: "Neighborhoods",
caption: "Neighborhood boundaries for the city", // Optional
description: "This layer shows the official neighborhood boundaries" // Optional
});
if (layerResult) {
console.log("Created layer group:", layerResult.layerGroup);
console.log("Created layers:", layerResult.layers);
}// Assuming you have a File object from a file input
const fileInput = document.getElementById('geojson-upload');
const file = fileInput.files[0];
const layerResult = await felt.createLayersFromGeoJson({
source: {
type: "geoJsonFile",
file: file
},
name: "User Uploaded Data"
});
if (layerResult) {
// Store the layer ID for later reference
const layerId = layerResult.layers[0].id;
}const geojsonData = {
type: "FeatureCollection",
features: [
{
type: "Feature",
geometry: {
type: "Point",
coordinates: [-122.4194, 37.7749]
},
properties: {
name: "San Francisco",
population: 874961
}
},
// Additional features...
]
};
const layerResult = await felt.createLayersFromGeoJson({
source: {
type: "geoJsonData",
data: geojsonData
},
name: "Dynamic Points"
});const layerResult = await felt.createLayersFromGeoJson({
name: "Styled Features",
source: {
type: "geoJsonUrl",
url: "https://example.com/data/mixed-features.geojson"
},
geometryStyles: {
Point: {
paint: {
color: "red",
size: 8
}
},
Line: {
paint: {
color: "blue",
size: 4
},
config: {
labelAttribute: ["name"]
},
label: {
minZoom: 0
}
},
Polygon: {
paint: {
color: "green",
strokeColor: "darkgreen",
fillOpacity: 0.5
}
}
}
});await felt.deleteLayer("layer-1");const layerResult = await felt.createLayersFromGeoJson({
source: {
type: "geoJsonUrl",
url: "https://example.com/data/realtime-sensors.geojson",
refreshInterval: 60000 // Refresh every minute
},
name: "Live Sensor Data"
});await felt.updateLayer({
id: layer.id,
source: {
type: "geoJsonData",
data: updatedFeatureCollection,
},
});felt.updateUiControls({ showLegend: false });initialViewport: {
center: { latitude: 40.7128, longitude: -74.006 },
zoom: 10,
}const felt = await Felt.embed(container, FELT_MAP_ID, {
token: embedTokenFromYourServer,
});"filters": ["acres", "lt", 50000]"filters": [["acres", "ge", 50000], "and", ["acres", "le", 70000]]"filters": [["temperature", "ge", 0], "and", ["temperature", "le", 100]]"filters": [["name", "isnt", null], "and", ["name", "ne", ""]]// WRONG: [a, "and", b, "and", c]
// RIGHT:
"filters": [["a", "eq", 1], "and", [["b", "eq", 2], "or", ["c", "eq", 3]]]"legend": {}"legend": {
"displayName": {
"13": "13: 60 to 70 °F",
"12": "12: 50 to 60 °F",
"11": "11: 40 to 50 °F",
"10": "10: 30 to 40 °F",
"9": "9: 20 to 30 °F",
"8": "8: 10 to 20 °F",
"7": "7: 0 to 10 °F",
"6": "6: -10 to 0 °F",
"5": "5: -20 to -10 °F",
"4": "4: -30 to -20 °F",
"3": "3: -40 to -30 °F",
"2": "2: -50 to -40 °F",
"1": "1: -60 to -50 °F"
}
}"legend": {
"displayName": {
"0": "5.14 to 19.46",
"1": "19.46 to 26.43",
"2": "26.43 to 34.06",
"3": "34.06 to 45.06",
"4": "45.06 to 100"
}
}"legend": {
"displayName": {
"0": "2.34M",
"1": "714.65K",
"2": "33K"
}
}"legend": {
"displayName": {
"0": "Low",
"1": "High"
}
}curl \
-X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${FELT_API_TOKEN}" \
"https://felt.com/api/v2/maps/${MAP_ID}/elements" \
-d '{"type":"FeatureCollection","features":[{"type":"Feature","properties":{},"geometry":{"coordinates":[[[15.478752514432728,15.576176978045694],[15.478752514432728,4.005934587045303],[29.892174099255755,4.005934587045303],[29.892174099255755,15.576176978045694],[15.478752514432728,15.576176978045694]]],"type":"Polygon"}}]}'new_elements = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
# Include "felt:id" in properties to update an existing annotation.
# "felt:" properties control styling; anything else is stored as data.
"properties": {"felt:color": "#2674BA"},
"geometry": {
"coordinates": [
[
[
15.478752514432728,
15.576176978045694
],
[
15.478752514432728,
4.005934587045303
],
[
29.892174099255755,
4.005934587045303
],
[
29.892174099255755,
15.576176978045694
],
[
15.478752514432728,
15.576176978045694
]
]
],
"type": "Polygon"
}
}
]
}
r = requests.post(
f"https://felt.com/api/v2/maps/{map_id}/elements",
headers={"Authorization": f"Bearer {api_token}"},
json=new_elements
)
assert r.ok
print(r.json())from felt_python import upsert_elements
new_elements = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {},
"geometry": {
"coordinates": [
[
[
15.478752514432728,
15.576176978045694
],
[
15.478752514432728,
4.005934587045303
],
[
29.892174099255755,
4.005934587045303
],
[
29.892174099255755,
15.576176978045694
],
[
15.478752514432728,
15.576176978045694
]
]
],
"type": "Polygon"
}
}
]
}
upsert_elements(map_id, new_elements)curl \
-X DELETE \
-H "Authorization: Bearer ${FELT_API_TOKEN}" \
"https://felt.com/api/v2/maps/${MAP_ID}/elements/${ELEMENT_ID}"element_id = "<YOUR_ELEMENT_ID>"
r = requests.delete(
f"https://felt.com/api/v2/maps/{map_id}/elements/{element_id}",
headers={"Authorization": f"Bearer {api_token}"},
)
assert r.okfrom felt_python import delete_element
element_id = "<YOUR_ELEMENT_ID>"
delete_element(map_id, element_id)import os
from felt_python import list_elements
# Setting your API token as an env variable can save
# you from repeating it in every function call
os.environ["FELT_API_TOKEN"] = "<YOUR_API_TOKEN>"
map_id = "<YOUR_MAP_ID>"
list_elements(map_id)curl \
-H "Authorization: Bearer ${FELT_API_TOKEN}" \
"https://felt.com/api/v2/maps/${MAP_ID}/element_groups"r = requests.get(
f"https://felt.com/api/v2/maps/{map_id}/element_groups",
headers={"Authorization": f"Bearer {api_token}"}
)
assert r.ok
print(r.json())from felt_python import list_element_groups
list_element_groups(map_id){
"body": {
"attributes": {
"type": "map:update",
"updated_at": "2024-04-29T12:16:46",
"map_id": "Jzjr8gMKSrCOxZ1OSMT49CB"
}
}
}import { Felt } from "@feltmaps/js-sdk";
const felt = await Felt.embed(
document.getElementById("container"),
FELT_MAP_ID,
);const layer = await felt.getLayer("layer-1");
felt.getElements().then(elements => {
console.log(elements);
});const layer = await felt.getLayer("layer-1");
const element = await felt.getElement("element-1");const layers = await felt.getLayers({ ids: ["layer-1", "layer-2"] });
const legendItems = await felt.getLegendItems({ layerIds: ["layer-1", "layer-2"] });const layers = await felt.getLayers();
const legendItems = await felt.getLegendItems();const layer = await felt.getLayer("layer-1");
if (layer) {
// Layer exists, safe to use
console.log(layer.visible);
} else {
console.log("Layer not found");
}// Better approach
const layers = await felt.getLayers({ ids: ["layer-1", "layer-2"] });
// Less efficient approach
const layer1 = await felt.getLayer("layer-1");
const layer2 = await felt.getLayer("layer-2");const unsubscribe = felt.onLayerChange({
options: { id: "layer-1" },
handler: ({ layer }) => {
console.log("Layer updated:", layer);
}
});const selection = await felt.getSelection();
const unsubscribe = felt.onSelectionChange({
handler: ({ selection }) => {
console.log("Selection updated:", selection);
}
});const filters = await felt.getLayerFilters("layer-1");
await felt.setLayerFilters({
layerId: "layer-1",
filters: ["name", "eq", "Jane"],
});
const unsubscribe = felt.onLayerFiltersChange({
options: { layerId: "layer-1" },
handler: (filters) => console.log(filters.combined),
});const unsubscribe = felt.onLayerChange({
options: { id: "layer-1" },
handler: ({ layer }) => {
console.log("Layer changed:", layer);
}
});
// Later, when you're done listening:
unsubscribe();useEffect(() => {
const unsubscribe = felt.onViewportMove({
handler: (viewport) => {
console.log("Viewport changed:", viewport);
}
});
// Clean up when the component unmounts
return () => unsubscribe();
}, []);// Current API
felt.onElementChange({
options: { id: "element-1" },
handler: ({ element }) => { /* ... */ }
});
// If we need to add new options later, no breaking changes:
felt.onElementChange({
options: {
id: "element-1",
newOption: "value" // Can add new options without breaking existing code
},
handler: ({ element }) => { /* ... */ }
});felt.onSelectionChange({
handler: ({ selection }) => {
selection.forEach(node => {
console.log(node.type); // e.g., "element", "layer", "feature", ...
console.log(node.entity); // The actual entity object
if (node.type === "element") {
// TypeScript knows this is an Element
console.log(node.entity.attributes);
}
});
}
});function MyComponent() {
// get the felt controller (or null if it's not loaded yet) and a ref to the map container
// into which we can embed the map
const { felt, mapRef } = useFeltEmbed("FELT_MAP_ID", {
uiControls: {
cooperativeGestures: false,
fullScreenButton: false,
showLegend: false,
},
});
return (
<div>
{/* the map container — remember to give it a height in your CSS */}
<div ref={mapRef} />
{/* a component that uses the Felt controller */}
<MyFeltApp felt={felt} />
</div>
);
}import {
Felt,
FeltController,
FeltEmbedOptions,
Layer,
} from "@feltmaps/js-sdk";
import React from "react";
export function useFeltEmbed(mapId: string, embedOptions: FeltEmbedOptions) {
const [felt, setFelt] = React.useState<FeltController | null>(null);
const hasLoadedRef = React.useRef(false);
const mapRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
async function loadFelt() {
if (hasLoadedRef.current) return;
if (!mapRef.current) return;
hasLoadedRef.current = true;
const felt = await Felt.embed(mapRef.current, mapId, embedOptions);
setFelt(felt);
}
loadFelt();
}, []);
return {
felt,
mapRef,
};
}export function useLiveLayer(felt: FeltController, initialLayer: Layer) {
// start with the layer we were given
const [currentLayer, setLayer] = React.useState<Layer | null>(initialLayer);
// listen for changes to the layer and update our state accordingly
// (onLayerChange returns its unsubscribe function, so returning it
// from the effect cleans up the listener on unmount)
React.useEffect(() => {
return felt.onLayerChange({
options: { id: initialLayer.id },
handler: ({ layer }) => setLayer(layer),
});
}, [felt, initialLayer.id]);
// return the live layer
return currentLayer;
}{
"attributes": {
"ele": {"displayName": "Elevation (meters)"},
"faa": {"displayName": "FAA Code"},
"iata": {"displayName": "IATA Code"},
"icao": {"displayName": "ICAO Code"},
"name": {"displayName": "Name"},
"name_en": {"displayName": "Name (EN)"},
"wikipedia": {"displayName": "Wikipedia entry"}
},
"config": {"labelAttribute": ["name_en", "name"]},
"filters": [["name", "isnt", null], "and", ["name", "ne", ""]],
"label": {
"color": "hsl(40,30%,40%)",
"fontSize": {"linear": [[12, 12], [20, 20]]},
"fontStyle": "Normal",
"fontWeight": 400,
"haloColor": "hsl(40,20%,85%)",
"haloWidth": 1.5,
"justify": "auto",
"letterSpacing": 0.1,
"lineHeight": 1.2,
"maxLineChars": 10,
"maxZoom": 23,
"minZoom": 10,
"offset": [8, 0],
"padding": 10,
"placement": ["E", "W"]
},
"legend": {},
"paint": {
"color": "hsl(40,30%,80%)",
"highlightColor": "#EA3891",
"highlightStrokeColor": "#EA3891",
"highlightStrokeWidth": {"linear": [[3, 0], [20, 2]]},
"isSandwiched": false,
"opacity": 1,
"size": {"linear": [[3, 1], [20, 6]]},
"strokeColor": "hsl(40,20%,55%)",
"strokeWidth": {"linear": [[3, 0.5], [20, 2]]}
},
"version": "2.3.1"
}{
"version": "2.3.1",
"type": "simple",
"filters": ["status", "in", ["active"]],
"paint": {
"iconImage": "hospital",
"iconFrame": "frame-circle",
"color": "#E74C3C",
"size": 4,
"opacity": 0.9,
"isClickable": true,
"isHoverable": true
},
"legend": {}
}{
"version": "2.3.1",
"type": "simple",
"config": {"labelAttribute": ["name"]},
"paint": [
{"color": "#FFFFFF", "size": 4, "lineCap": "round", "lineJoin": "round", "isClickable": true},
{"color": "#374151", "size": 8, "lineCap": "round", "lineJoin": "round"}
],
"label": {
"color": "auto",
"haloColor": "auto",
"haloWidth": 2,
"placement": "Above",
"minZoom": 12
},
"legend": {}
}await felt.selectFeature({
id: "feature-123",
layerId: "buildings-layer"
});await felt.selectFeature({
id: "feature-123",
layerId: "buildings-layer",
showPopup: false, // Whether to show the feature popup (default: true)
fitViewport: { maxZoom: 15 } // Fit viewport to feature with zoom limit
});const selection = await felt.getSelection();
console.log(`${selection.length} items selected`);
selection.forEach(node => {
switch(node.type) {
case 'feature':
console.log('Selected feature:', node.entity.id, 'from layer:', node.entity.layerId);
break;
case 'element':
console.log('Selected element:', node.entity.name || node.entity.type);
break;
}
});// Clear all selections
await felt.clearSelection();
// Clear specific types of selections
await felt.clearSelection({
features: true, // Clear feature selections
elements: false // Keep element selections
});const unsubscribe = felt.onSelectionChange({
handler: ({ selection }) => {
// selection is an array of EntityNode objects
console.log("Selected entities:", selection);
// Check what's selected
selection.forEach(node => {
console.log("Entity type:", node.type);
console.log("Entity ID:", node.entity.id);
});
}
});
// Don't forget to clean up when you're done
unsubscribe();const unsubscribe = felt.onSelectionChange({
handler: ({ selection }) => {
// Handle selection...
}
});
// Later, when you're done:
unsubscribe();const unsubscribe = felt.onSelectionChange({
handler: ({ selection }) => {
if (selection.length === 0) {
console.log("Nothing is selected");
return;
}
// Handle selection...
}
});const viewport = await felt.getViewport();
console.log(viewport.center); // { latitude: number, longitude: number }
console.log(viewport.zoom); // numberfelt.setViewport({
center: {
latitude: 37.7749,
longitude: -122.4194
},
zoom: 12
});felt.fitViewportToBounds({
bounds: [
west, // minimum longitude
south, // minimum latitude
east, // maximum longitude
north // maximum latitude
]
});const unsubscribe = felt.onViewportMove({
handler: (viewport) => {
console.log("New center:", viewport.center);
console.log("New zoom:", viewport.zoom);
}
});
// Clean up when done
unsubscribe();const unsubscribe = felt.onPointerClick({
handler: (event) => {
// Location of the click
console.log("Click location:", event.coordinate);
// Features under the click
console.log("Clicked features:", event.features);
// The pixel coordinates of the cursor, measured from the top left corner of the map DOM element.
console.log("Screen coordinates:", event.point);
// Values from raster layers under the pointer.
// Always an array - empty when no raster layers are hit.
for (const {value, categoryName, color, layerId} of event.rasterValues) {
console.log("Raster value:", value, categoryName);
}
}
});const unsubscribe = felt.onPointerMove({
handler: (event) => {
// Current mouse location
console.log("Mouse location:", event.coordinate);
// Features under the cursor
console.log("Hovered features:", event.features);
// The pixel coordinates of the cursor, measured from the top left corner of the map DOM element.
console.log("Screen coordinates:", event.point);
// Values from raster layers under the pointer.
// Always an array - empty when no raster layers are hit.
for (const {value, categoryName, color, layerId} of event.rasterValues) {
console.log("Raster value:", value, categoryName);
}
}
});const unsubscribe = felt.onPointerMove({
handler: (event) => {
// Handle event...
}
});
// Later, when you're done:
unsubscribe();import { throttle } from "lodash";
felt.onPointerMove({
handler: throttle((event) => {
// Handle frequent mouse moves...
}, 100) // Limit to once every 100ms
});onViewportMove
—
ViewportState — fires continuously during movement, including during animations and inertia.
onViewportMoveEnd
—
ViewportState — fires once when dragging, zooming, animations, and inertia have finished.
onMapIdle
—
Nothing. Fires when the map is fully idle: no transitions, no interaction, all tiles loaded, fades complete.
onBasemapChange
—
Basemap — the new basemap.
onElementCreate
—
{ element: Element | null, isBeingCreated: boolean } — fires repeatedly while the user draws (isBeingCreated: true), then a final time with isBeingCreated: false.
onElementCreateEnd
—
onLayerChange
{ id }
{ layer: Layer | null }
onLayerGroupChange
{ id }
onPointerClick
—
MapInteractionEvent — { coordinate, point, features, rasterValues }. features and rasterValues are arrays (empty when nothing is under the pointer).
onPointerMove
—
onToolChange
—
ToolType | null — the newly selected tool, or null when no tool is active.
onToolSettingsChange
—
For payload type details, follow each method's entry in the API reference.
const unsubscribe = felt.onLayerChange({
options: { id: "layer-1" },
handler: ({ layer }) => console.log(layer.bounds),
});
// ...later, when you no longer need the listener
unsubscribe();binMode
fixed (default), low, medium, high, or auto. fixed keeps the cell resolution constant at all zooms; the other modes pick a resolution from the current zoom, where low produces larger hexagons and high smaller ones (auto behaves like medium).
baseBinLevel
Required. If binMode is fixed, this is the that the map will use. H3 cells vary from resolution 0 (largest) to resolution 15 (smallest). In the zoom-adaptive modes, this is the reference resolution used for calculating class breaks — you will get best results choosing a resolution that matches the fixed resolution you would choose for the most commonly-viewed zoom of your map.
numericAttribute
The numeric column to aggregate. Required unless aggregation is count , in which case the column choice is irrelevant.
This is an example of an H3 visualization
defined with the following style
aggregation: "count" doesn't need a numericAttribute — it counts the points in each cell:
H3 cells display aggregated values, not raw feature attributes. Their popups reference special attribute names — "felt:cluster_size" for the feature count, and "felt:sum:column", "felt:mean:column", "felt:min:column", "felt:max:column" for aggregates. If no popup is provided, Felt auto-generates sensible stats from the aggregation type. See the popup block.
aggregation
The aggregation method that will be used on points within each cell. Supported values are count (default), sum, min, max, and mean .
{
"config": {
"steps": {"type": "quantiles", "count": 5},
"aggregation": "sum",
"binMode": "fixed",
"baseBinLevel": 3,
"numericAttribute": "capacity_mw"
},
"paint": {"color": "@riverine"},
"type": "h3",
"version": "2.3.1",
"label": {},
"legend": {"displayName": "auto"}
}{
"version": "2.3.1",
"type": "h3",
"config": {
"aggregation": "count",
"baseBinLevel": 4,
"binMode": "medium",
"steps": {"type": "continuous"}
},
"paint": {"color": "@galaxy", "opacity": 0.85, "strokeColor": "auto", "strokeWidth": 1},
"legend": {"displayName": "auto"}
}{
"version": "2.3.1",
"type": "h3",
"config": {
"numericAttribute": "revenue",
"aggregation": "sum",
"baseBinLevel": 5,
"binMode": "medium",
"steps": {"type": "jenks", "count": 5}
},
"paint": {"color": "@copper", "opacity": 0.85, "strokeColor": "auto", "strokeWidth": 1, "isClickable": true},
"popup": {
"titleAttribute": "felt:sum:revenue",
"keyAttributes": ["felt:cluster_size", "felt:mean:revenue", "felt:min:revenue", "felt:max:revenue"]
},
"attributes": {"revenue": {"displayName": "Revenue ($)", "format": {"thousandSeparated": true, "mantissa": 0}}},
"legend": {"displayName": "auto"}
}The Global Power Plants layer in Felt is an example of a categorical layer on a vector dataset
and is defined by the following style
Notice that we are saying that the primary_fuel data attribute will be used to categorize elements and that the possible values of that attribute that we are interested in are "Solar", "Hydro", "Wind", "Gas", "Coal", "Oil" and "Nuclear" (colors are assigned in the same order). Also notice that we are defining either a single value that will apply to all categories (i.e. size) or a value for each category (i.e. color)
Palette shortcut + "top N". When you don't know the exact category values, let Felt pick the most common ones and color them from a categorical palette:
A different icon per category. iconImage and the paint arrays follow the same N (or N + 1 with showOther) pattern as color:
Styling "Other" separately. With showOther: true, the trailing array element targets the "Other" bucket — here it is made smaller and more transparent than the named categories:
{
"version": "2.3.1",
"type": "categorical",
"config": {
"categoricalAttribute": "primary_fuel",
"categories": ["Solar", "Hydro", "Wind", "Gas", "Coal", "Oil", "Nuclear"],
"showOther": false
},
"legend": {"displayName": {}},
"attributes": {
"capacity_mw": {"displayName": "Capacity (MW)"},
"name": {"displayName": "Name"},
"primary_fuel": {"displayName": "Primary Fuel"}
},
"paint": {
"color": [
"#E5C550",
"#7AB6C2",
"#AB71A4",
"#CC615C",
"#AD7B68",
"#EB9360",
"#DEA145"
],
"isSandwiched": false,
"opacity": 1,
"size": [{"linear": [[3, 1.5], [20, 8]]}]
}
}{
"version": "2.3.1",
"type": "categorical",
"config": {
"categoricalAttribute": "fuel_type",
"categories": {"type": "top", "count": 10},
"showOther": true
},
"paint": {
"color": "@catPalette4",
"size": 4,
"strokeColor": "auto",
"strokeWidth": 1,
"opacity": 0.9,
"isClickable": true
},
"legend": {"displayName": "auto"}
}{
"version": "2.3.1",
"type": "categorical",
"config": {
"categoricalAttribute": "facility_type",
"categories": ["hospital", "school", "fire_station"],
"showOther": true
},
"paint": {
"iconImage": ["hospital", "school", "fire", "dot"],
"iconFrame": "frame-circle",
"color": ["#E74C3C", "#3498DB", "#E67E22", "#95A5A6"],
"size": 4,
"opacity": 0.9,
"isClickable": true
},
"legend": {
"displayName": {
"hospital": "Hospitals",
"school": "Schools",
"fire_station": "Fire Stations"
}
}
}{
"version": "2.3.1",
"type": "categorical",
"config": {
"categoricalAttribute": "status",
"categories": ["active", "pending", "closed"],
"showOther": true
},
"paint": {
"color": ["#22C55E", "#F59E0B", "#EF4444", "#9CA3AF"],
"size": [8, 8, 8, 4],
"opacity": [0.9, 0.9, 0.9, 0.5],
"strokeColor": "auto",
"strokeWidth": 1
},
"legend": {"displayName": "auto"}
}The following image shows the behavior of this definition:
{ "linear": Stops[] }: Linearly interpolates between stop values less than or equal and greater than the input value
The following image shows the behaviour of this definitions
{ "linear": [number, number] }: Expands to { "linear": [[minZoom, number], [maxZoom, number]] }
Color linear interpolation is done in the HCL color-space
{ "exp": [number, Stops[]] }: Exponentially interpolates between output stop values less than or equal and greater than the input value. The base parameter controls the rate at which output increases where higher values increase the output value towards the end of the range, lower values increase the output value towards the start of the range, and a base 1 interpolates linearly.
The used value is computed as follows : (Math.pow(base, progress) - 1) / (Math.pow(base, difference) - 1)
The following images shows the behaviour of this definition
{ "cubicbezier": [number, number, number, number, Stops[]] }: Interpolates using the bezier curve defined by the curve control points.
The following images shows the behaviour of this definition
{ "step": ["hsl(50,5%,72%)", [[9, "hsl(10,75%,75%)"]]] }
// If zoom level is less than 9, "hsl(50,5%,72%)" will be returned
// If zoom level is equal or higher than 9, "hsl(10,75%,75%)" will be returned
{ "step": [0, [[0, 0], [100, 100]]]} // Blue
{ "step": [0, [[0, 0], [50, 50], [100, 100]]]} // Red
{ "step": [0, [[0, 0], [25, 25], [50, 50], [75, 75], [100, 100]]]} // Yellow
{
"linear": [
[8, 10],
[14, 15],
[20, 21]
]
}
// If zoom level is less than 8, 10 is returned
// If zoom level is greater or equal than 8 but less than 14, a value linearly interpolated
// between 10 and 15 is returned
// If zoom level is greater or equal than 14 but less than 20, a value linearly interpolated // between 15 and 21 is returned
// If zoom level is greater or equal than 20, 21 is returned
{ "linear": [[0, 0], [100, 100]]} // Blue
{ "linear": [[0, 0], [50, 50], [100, 100]]} // Red
{ "linear": [[0, 0], [25, 25], [50, 50], [75, 75], [100, 100]]} // Yellow
{ "linear": [8, 10] }
// If minZoom is defined as 3 and maxZoom is defined as 20:
// If zoom level is less than 3, 8 is returned
// If zoom level is between 3 and 20, a value linearly interpolated between 8 and 10 is
// returned
// If zoom level is greater or equal than 20, 10 is returned
{
"exp": [
0.25,
[
[0, 25],
[10, 100]
]
]
}
// If zoom level is less than 0, 25 is returned
// If zoom level z is between 0 and 10, an interpolation factor is computed between 0 and 10
// and then it's used to interpolate between 25 and 100
// If zoom level is equal or higher than 10, 100 will be returned
{ "exp": [0.25, [[0, 0], [100, 100]]]} // Blue
{ "exp": [0.25, [[0, 0], [50, 50], [100, 100]]]} // Red
{ "exp": [0.25, [[0, 0], [25, 25], [50, 50], [75, 75], [100, 100]]]} // Yellow{ "exp": [0.5, [[0, 0], [100, 100]]]} // Blue
{ "exp": [0.5, [[0, 0], [50, 50], [100, 100]]]} // Red
{ "exp": [0.5, [[0, 0], [25, 25], [50, 50], [75, 75], [100, 100]]]} // Yellow{ "exp": [0.75, [[0, 0], [100, 100]]]} // Blue
{ "exp": [0.75, [[0, 0], [50, 50], [100, 100]]]} // Red
{ "exp": [0.75, [[0, 0], [25, 25], [50, 50], [75, 75], [100, 100]]]} // Yellow{ "exp": [1, [[0, 0], [100, 100]]]} // Blue
{ "exp": [1, [[0, 0], [50, 50], [100, 100]]]} // Red
{ "exp": [1, [[0, 0], [25, 25], [50, 50], [75, 75], [100, 100]]]} // Yellow{ "exp": [1.25, [[0, 0], [100, 100]]]} // Blue
{ "exp": [1.25, [[0, 0], [50, 50], [100, 100]]]} // Red
{ "exp": [1.25, [[0, 0], [25, 25], [50, 50], [75, 75], [100, 100]]]} // Yellow{ "exp": [2, [[0, 0], [100, 100]]]} // Blue
{ "exp": [2, [[0, 0], [50, 50], [100, 100]]]} // Red
{ "exp": [2, [[0, 0], [25, 25], [50, 50], [75, 75], [100, 100]]]} // Yellow{ "cubicbezier": [0.25, 0, 0.75, 1.5, [[0, 0], [100, 100]]]} // Blue
{ "cubicbezier": [0.25, 0, 0.75, 1.5, [[0, 0], [50, 50], [100, 100]]]} // Red
{ "cubicbezier": [0.25, 0, 0.75, 1.5, [[0, 0], [25, 25], [50, 50], [75, 75], [100, 100]]]} // YellowThe config block contains configuration options for a given visualization.
These are the fields that each config block can contain:
aggregation
Optional, for visualizations. How points are aggregated within each cell: "count" (default), "sum", "mean", "min", or "max". All values except "count" also require numericAttribute.
config is always an object. The examples below show the config block in isolation.
Point layers can be drawn as icons instead of plain circles. Set iconImage in the paint block to a built-in icon slug (or an emoji), and the icon takes on the layer's color and size.
Icons work in simple, categorical, and numeric point visualizations. In categorical and numeric styles, iconImage (and the other paint properties) may be an array so each category or class gets its own marker.
When iconImage is set, color fills the icon and size scales it (the same size you would use for a circle radius).
A single icon marker, framed in a circle:
To give each category its own icon, make iconImage an array — see the icon-per-category example in .
Shapes: dot, square, diamond, triangle, x, plus, circle-line, circle-slash, circle-triangle, circle-x,
To use an emoji as a marker, write it as "emoji::name:" — note the double colon after emoji and the trailing colon. For example: "emoji::fire:", "emoji::tree:". iconFrame does not apply to emojis. Prefer built-in icons unless an emoji is specifically requested.
This is a sample application showing how to use the Felt SDK to build an app with the following features:
listing the map's layers
toggling layer visibility
moving the viewport to center it on predefined city locations
The commented code in its entirety is shown below.
The Felt Style Language (FSL) is a JSON-based specification for styling data layers in Felt — comparable to the Mapbox Style Spec, but at the layer level. A single JSON object describes how a layer is drawn (paint), labeled (label), classified (config), and presented in popups and legends.
The same style object works in three places:
In the Felt app — open a layer's overflow menu and choose Actions > Edit style language to view and edit the layer's FSL directly. (The neighbouring Actions > Edit styles opens the visual style editor, which has no JSON view.)
{ element: Element } — fires once, when creation is finished.
onElementChange
{ id }
{ element: Element | null, isBeingCreated: boolean } — element is null if it was removed.
onElementDelete
{ id }
Nothing.
onElementGroupChange
{ id }
{ elementGroup: ElementGroup | null }
{ layerGroup: LayerGroup | null }
onLegendItemChange
{ id, layerId }
{ legendItem: LegendItem | null }
onLayerFiltersChange
{ layerId }
LayerFilters — the bare filters object ({ style, components, ephemeral, combined }), with no wrapper. See Layer filters.
onLayerBoundariesChange
{ layerId }
LayerBoundaries | null
MapInteractionEvent
onSelectionChange
—
{ selection: EntityNode[] } — see Working with selection.
ToolSettingsChangeEvent — a discriminated union: check the tool field to narrow to that tool's settings.











band
Optional. Used in raster numeric visualizations. The raster band (1-indexed) to read data from — a number, or an array of band numbers for multiband styling.
baseBinLevel
Used in H3 visualizations. The H3 cell resolution (0–15). Country-scale ≈ 3–4, city-scale ≈ 6–7. Sets the cell size when binMode is "fixed"; in the zoom-adaptive modes it serves as the reference resolution that steps values are scaled against.
binMode
Used in H3 visualizations. "fixed" (default; uses baseBinLevel at all zooms) or the zoom-adaptive modes "low", "medium"/"auto", and "high" — low means larger hexagons, high means smaller ones.
categoricalAttribute
Mandatory for vector categorical visualizations. The attribute that contains the categorical values that will be used. (Raster categorical layers classify raw pixel values instead and omit this field.)
categories
Mandatory for a categorical visualization. Either an explicit array of category values, or a shortcut: {"type": "top", "count": N} for the N most common values, {"type": "bottom", "count": N} for the N least common, or {"type": "all"} (raster) for every unique pixel value.
labelAttribute
Optional. Defines which dataset attribute or attributes to use for labeling. If multiple values are provided, the first available one will be used.
method
Optional. Used in multiband raster numeric visualizations. Maps a spectral index (NDVI/NDMI/NDWI) to its band assignments. See Raster visualizations.
noData
Optional. Used in raster visualizations. A value or array of values that won’t be shown (e.g. [-9999] for voids, [0] for no-data).
numericAttribute
Mandatory for a numeric visualization. The attribute that contains the numeric values used.
otherOrder
Optional. Used in categorical visualizations. It can be set to either "below" or "above" to make features that do not match any of the defined categories render below or above the other ones. The default position is "below".
outOfRangeValues
Optional. Used in raster numeric visualizations. "color" clamps out-of-range pixels to the nearest class color; "hide" makes them transparent.
rasterResampling
Optional. Used in raster numeric (and tinted hillshade) visualizations — not valid for categorical rasters, which always resample nearest. "nearest" preserves exact pixel values; "linear" smoothly interpolates (use for continuous imagery/elevation). Set it explicitly rather than relying on a default.
showOther
Optional. Used in categorical visualizations. If set to true it shows all features that do not match any defined category and adds an extra entry as the last item in the legend.
steps
Used by numeric, H3, and tinted-hillshade visualizations. Either an explicit array of break points (at least two values), or a classification shortcut such as {"type": "jenks", "count": 5} or {"type": "continuous"}.
number
Zoom level below which icons are drawn as plain points instead. Optional.
circle-plusstarhearthexagonoctagonArrows & directions: arrow-up, chevron-up, double-chevron-up, direction-up (combine with iconRotation to point them anywhere)
Transportation: pedestrian, bicycle, wheelchair, airport, car, bus, train, truck, ferry, sailboat, electric-service, gas-service, blood-clinic, badge, traffic-light, traffic-cone, road-sign-caution
Activities & places: person, restroom, house, work, letter, hotel, factory, hospital, religious-facility, school, government, university, bank, landmark, museum, clothing, shopping, store, bar, pub, cafe, food, park, amusement-park, camping-tent, cabin, picnic, water-refill, trailhead, guidepost, viewpoint, camera, us-football, football, tennis, binoculars, swimming
Infrastructure: zap, battery-full, battery-half, battery-low, boom, radar, wind-turbine, solar-panel, antenna, telephone-pole, oil-well, oil-barrel, railroad-track, bridge, lighthouse, lock-closed, lock-open, wifi, trash, recycle
Nature: tree, flower, leaf, fire, mountain, snowy-mountain, volcano, island, wave, hot-springs, water, lake, ocean, animal, bird, duck, dog, fish, beach, wetland
Weather: sun, moon, cloud, partial-sun, rain, lightning, snowflake, wind, snow, fog, sleet, hurricane
Signs: warning, parking, info, circle-exclamation
iconImage
string | string[]
An icon slug from the catalog below, or an emoji in the form "emoji::name:".
iconFrame
string
"none", "frame-circle", or "frame-square". Built-in icons only (not emojis).
iconRotation
number | string
Rotation in degrees, or the name of a column to rotate by.
Use only the icon slugs listed here. Invented names will not render.
iconHideOnZoom

"config": {
"labelAttribute": ["Wikipedia", "faa"],
"categoricalAttribute": "faa",
"categories": ["faa-code-1", "faa-code-2", "faa-code-3"],
"showOther": true,
"otherOrder": "above"
}"config": {
"categoricalAttribute": "surface",
"categories": {"type": "top", "count": 5},
"showOther": true
}"config": {
"numericAttribute": "percentage",
"steps": {"type": "jenks", "count": 5}
}"config": {
"band": 1,
"steps": {"type": "continuous"},
"noData": [-9999],
"rasterResampling": "nearest"
}"config": {
"aggregation": "sum",
"numericAttribute": "capacity_mw",
"baseBinLevel": 4,
"binMode": "medium",
"steps": {"type": "quantiles", "count": 5}
}{
"version": "2.3.1",
"type": "simple",
"paint": {
"iconImage": "hospital",
"iconFrame": "frame-circle",
"color": "#E74C3C",
"size": 4,
"opacity": 0.9,
"isClickable": true
},
"legend": {}
}{
"version": "2.3.1",
"type": "simple",
"paint": {
"iconImage": "emoji::fire:",
"size": 6
},
"legend": {}
}<!doctype html>
<html lang="en">
<head>
<title>Felt JS SDK</title>
<style>
body {
margin: 0;
padding: 0;
font-family: sans-serif;
font-size: 13px;
}
.container {
display: grid;
grid-template-columns: 1fr 240px;
height: 100vh;
}
iframe {
display: block;
}
#sidebar {
padding: 1rem;
user-select: none;
}
#markers {
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid #ccc;
}
.marker {
cursor: pointer;
padding: 0.25rem 0;
}
.layer-toggles_toggle {
padding: 0.25rem 0;
margin-left: -4px;
display: flex;
align-items: center;
gap: 0.25rem;
}
h3 {
margin: 0;
margin-bottom: 0.5rem;
}
</style>
</head>
<body>
<div class="container">
<div id="mapContainer"></div>
<div id="sidebar">
<div id="markers">
<h3>Cities</h3>
</div>
<div id="layers">
<h3>Layers</h3>
</div>
</div>
</div>
<script type="module">
// Load the Felt SDK from the jsDelivr CDN
import { Felt } from "https://esm.run/@feltmaps/js-sdk";
// Get the map and sidebar elements
const container = document.getElementById("mapContainer");
const markerContainer = document.getElementById("markers");
const layerContainer = document.getElementById("layers");
// Embed the map — replace this ID with your own map's ID
const felt = await Felt.embed(container, "u49BWs5EtSI29CpwuwB9CzRiC", {
uiControls: {
showLegend: false,
cooperativeGestures: false,
fullScreenButton: false,
},
});
// Add some cities to the sidebar
const locations = [
{ name: "Oakland", lat: 37.8044, lng: -122.271 },
{ name: "New York", lat: 40.7128, lng: -74.006 },
{ name: "Los Angeles", lat: 34.0522, lng: -118.2437 },
{ name: "Chicago", lat: 41.8781, lng: -87.6298 },
{ name: "Houston", lat: 29.7604, lng: -95.3698 },
{ name: "Phoenix", lat: 33.4484, lng: -112.074 },
];
locations.forEach((location) => {
// create a DOM element with the city name
const marker = document.createElement("div");
marker.classList.add("marker");
marker.innerText = location.name;
// center the viewport on the city when the marker is clicked
marker.addEventListener("click", () => {
felt.setViewport({
center: {
latitude: location.lat,
longitude: location.lng,
},
zoom: 10,
});
});
// add the marker to the sidebar
markerContainer.appendChild(marker);
});
// get all the layers
felt.getLayers().then((layers) => {
layers.forEach((layer) => {
// create a DOM element to represent the layer
const layerElement = document.createElement("div");
layerElement.classList.add("layer-toggles_toggle");
layerElement.innerHTML = `
<input type="checkbox" id="${layer.id}" ${
layer.visible ? "checked" : ""
}>
<label for="${layer.id}">${layer.name}</label>
`;
// toggle the layer's visibility when the checkbox changes
const checkbox = layerElement.querySelector("input");
checkbox.addEventListener("change", () => {
felt.setLayerVisibility(
checkbox.checked ? { show: [layer.id] } : { hide: [layer.id] },
);
});
// let the map be the source of truth: keep the checkbox in
// sync if the layer's visibility changes for any other reason
felt.onLayerChange({
options: { id: layer.id },
handler: ({ layer }) => {
if (layer) checkbox.checked = layer.visible;
},
});
// add the layer element to the container
layerContainer.appendChild(layerElement);
});
});
</script>
</body>
</html>Via the REST API — read a layer's style field and update it with POST /update_style.
Via the JS SDK — apply session-only style changes with setLayerStyle.
Every style needs a version and a type. Here is a complete, minimal style that draws a point layer in green with a visible stroke:
Paste this into Actions > Edit style language on any point layer and the points turn green immediately. From here, styling is incremental: change "type" to "categorical" or "numeric" to drive color or size from your data, add steps or categories to config, and swap literal colors for @palette shortcuts.
version
The FSL version the style is written against. Use "2.3.1" (current). Older versions are accepted and migrated automatically.
type
The visualization type: simple, categorical, numeric, heatmap, h3, or hillshade. See .
config
Learn how to define and configure the code blocks that compose the Felt Style Language
Learn about visualization types, including simple, categorical, numeric (color by & size by), heatmaps, H3, and raster (imagery, numeric, categorical, and hillshade).
Shared building blocks referenced throughout the language: smart "auto" colors and named palettes, the icon catalog, and the classification methods used to turn numbers into classes.
Vary paint and label properties with the map's zoom level using interpolators.
Details on how to customize legends on a per-layer basis.
Definitions for errors raised when validating the Felt Style Language.
A compact gallery with one worked example per visualization type.
{
"version": "2.3.1",
"type": "simple",
"config": {},
"paint": {
"color": "#28A745",
"size": 8,
"strokeColor": "auto",
"strokeWidth": 1
},
"legend": {},
"label": {},
"popup": {}
}The Felt SDK provides powerful methods to analyze your geospatial data and transform it into informative visualizations. You can calculate statistics on entire datasets or focus on specific areas using boundaries and filters, allowing you to create custom charts that reveal insights about your spatial data.
Statistics are available for layers that have been uploaded to and processed by Felt — layers created at runtime with createLayersFromGeoJson cannot be queried for statistics.
The SDK offers three complementary approaches to analyze your map data:
Calculate individual values (count, sum, average, etc.) across your dataset or a filtered subset. If no aggregation method is provided, the count is returned.
// Count all residential buildings
const residentialCount = await felt.getAggregates({
layerId: "buildings",
filters: ["type", "eq", "residential"]
});
// returns { count: 427 }
// Calculate average home value in a specific neighborhood
const avgHomeValue = await felt.getAggregates({
layerId: "buildings",
boundary: [-122.43, 47.60, -122.33, 47.62], // neighborhood boundary
aggregation: {
method: "avg",
attribute: "assessed_value"
}
});
// returns { avg: 652850.32 }Group features by unique attribute values and calculate statistics for each group.
// Basic grouping: Count of buildings by type
const buildingsByType = await felt.getCategoryData({
layerId: "buildings",
attribute: "type"
});
/* returns:
[
{ value: "residential", count: 427 },
{ value: "commercial", count: 82 },
{ value: "mixed-use", count: 38 },
{ value: "industrial", count: 15 }
]
*/Create bins for numeric data and calculate statistics for each range.
// Basic histogram: Building heights in 5 natural break bins
const buildingHeights = await felt.getHistogramData({
layerId: "buildings",
attribute: "height",
steps: { type: "jenks", count: 5 }
});
/* returns:
[
{ min: 0, max: 20, count: 175 },
{ min: 20, max: 50, count: 203 },
{ min: 50, max: 100, count: 142 },
{ min: 100, max: 200, count: 36 },
{ min: 200, max: 500, count: 6 }
]
*/You can apply filters in two powerful ways:
At the top level - Affects both which data is included and how values are calculated
In the values configuration - Only affects the calculated values while keeping all categories/bins
This two-level filtering is especially useful for creating comparative visualizations while maintaining consistent groupings.
Comparing building types by floor area (Categories)
Comparing building heights across time periods (Histograms)
Comparing neighborhood density (Aggregates)
Here's how you might integrate these analysis methods with an interactive chart:
This example demonstrates how a user clicking on a pie chart slice could apply a filter to the map, highlighting only the buildings of that type. It also shows how you could fetch additional statistics based on the user's selection to enrich the visualization experience.
Data configuration: which attribute drives the visualization, class breaks, categories, aggregation. See The config block.
paint
How geometries and pixels are drawn: colors, sizes, strokes, opacity, icons. See The paint block.
label
Feature labels: fonts, halos, placement, zoom range. See The label block.
popup
What clicking a feature shows. See The popup block.
attributes
Display names and number formatting for attributes. See The attributes block.
filters
Which features are visible. See The filters block.
legend
Legend labels. See Legends.
// Advanced: Show all building types, but only sum floor area of recent buildings
const recentBuildingAreaByType = await felt.getCategoryData({
layerId: "buildings",
attribute: "type",
values: {
filters: ["year_built", "ge", 2000],
aggregation: {
method: "sum",
attribute: "floor_area"
}
}
});
/* returns:
[
{ value: "residential", sum: 1250000 },
{ value: "commercial", sum: 750000 },
{ value: "mixed-use", sum: 350000 },
{ value: "industrial", sum: 120000 }
]
*/// Compare old vs new buildings using the same height ranges
const oldBuildingHeights = await felt.getHistogramData({
layerId: "buildings",
attribute: "height",
steps: [0, 20, 50, 100, 200, 500],
values: {
filters: ["year_built", "lt", 1950]
}
});
/* returns:
[
{ min: 0, max: 20, count: 96 },
{ min: 20, max: 50, count: 104 },
{ min: 50, max: 100, count: 37 },
{ min: 100, max: 200, count: 12 },
{ min: 200, max: 500, count: 1 }
]
*/
const newBuildingHeights = await felt.getHistogramData({
layerId: "buildings",
attribute: "height",
steps: [0, 20, 50, 100, 200, 500], // Same ranges as above
values: {
filters: ["year_built", "ge", 1950]
}
});
/* returns:
[
{ min: 0, max: 20, count: 79 },
{ min: 20, max: 50, count: 99 },
{ min: 50, max: 100, count: 105 },
{ min: 100, max: 200, count: 24 },
{ min: 200, max: 500, count: 5 }
]
*/// Find average residential density across different neighborhoods
const downtownDensity = await felt.getAggregates({
layerId: "buildings",
boundary: [-122.335, 47.600, -122.330, 47.610], // downtown boundary
filters: ["type", "eq", "residential"],
aggregation: {
method: "avg",
attribute: "units_per_acre"
}
});
// returns { avg: 124.7 }
const suburbanDensity = await felt.getAggregates({
layerId: "buildings",
boundary: [-122.200, 47.650, -122.150, 47.700], // suburban boundary
filters: ["type", "eq", "residential"],
aggregation: {
method: "avg",
attribute: "units_per_acre"
}
});
// returns { avg: 8.2 }// Create a pie chart showing building type distribution
async function createBuildingTypePieChart() {
// Get data for the chart
const data = await felt.getCategoryData({
layerId: "buildings",
attribute: "type"
});
// Render pie chart (using a hypothetical chart library)
const chart = renderPieChart(data, {
valuePath: "count",
labelPath: "value",
onSliceClick: handleSliceClick
});
return chart;
}
// Handle user interaction with the chart
async function handleSliceClick(slice) {
const buildingType = slice.label;
// Apply filter to highlight this building type on the map
await felt.setLayerFilters({
layerId: "buildings",
filters: ["type", "eq", buildingType],
note: `Showing ${buildingType} buildings only`
});
// Get additional statistics for this building type
const stats = await felt.getAggregates({
layerId: "buildings",
filters: ["type", "eq", buildingType],
aggregation: {
method: "avg",
attribute: "year_built"
}
});
// Update the UI with these statistics
updateStatsPanel(`Average ${buildingType} year built: ${Math.round(stats.avg)}`);
}
// Initialize the chart when the page loads
createBuildingTypePieChart();References have been updated in the app and documentation, while REST API endpoint and JS SDK method naming remain unchanged.
The Felt SDK provides two main approaches for creating annotations on your maps:
Interactive Drawing: Configure and activate drawing tools for users to create annotations manually
Programmatic Creation: Create and modify annotations directly through code
Annotations created via the SDK are session-specific - they're not persisted to the map and won't be visible to other users.
The methods on the ToolsController enable you to programmatically activate drawing tools for your users, as well as setting various options for the tools, such as color, line width, etc.
Use the method to activate a particular tool.
Use the method to configure the options for a specific tool.
Use the and to be notified of changes to the above, or read them instantaneously with the and methods.
As the user creates annotations with the tools, you can be notified of them being created and updated using the and listeners. See for more details.
If you want to create annotations programmatically instead of letting your users draw them interactively on the map, use the methods in the .
To create annotations, use the method.
To update annotations, use the method.
To delete annotations, use the method.
When annotations are created programmatically, they also trigger notifications about the corresponding changes to annotations, via onElementCreate, onElementChange and onElementDelete.
Extract the geometric representation of annotations using the method.
The geometry is returned in GeoJSON geometry format, which can be quite different to the way the annotation is specified in Felt. For example, Circle annotations in Felt have their geometry converted into a polygon, representing the area covered by the circle.
Note: Text, Note, and Image annotations do not return geometry as they are considered screen annotations rather than true "geospatial" annotations.
Every change that is made to the annotations on a map results in a call to either , or .
There are two different ways for listening to annotations being created, and the one you use depends on how the annotation is being created, and at what point you want to know about an annotation's creation.
When the user is creating annotations with tools, they are often created in a number of steps, such as drawing a marker stroke or creating a polygon with many vertices.
When you want to know when the user has finished creating the annotation (e.g. the polygon was closed or the marker stroke ended) then you should use the listener.
When annotations are created programmatically, they do not trigger the event.
Annotations created using Tools or will trigger the event, with an extra property stating whether the annotation is still being created.
Here is an example showing the power of the Felt SDK, where in just a few lines of code you can allow your users to draw annotations and have them sent to your own backend systems for persistence or analysis.
Assuming you have embedded your Felt map as described in , and in your own UI you have added a polygon-tool button and a reset-tool button, all you need is the following:
Raster layers are styled with one of five type values. They share a common set of config options and never support popups or labels.
Mode
type
Use it for
In paint, color takes a or an explicit color array, plus opacity and isSandwiched (render below basemap water/roads).
Renders the image with no classification — just opacity and resampling.
Classify one band. Prefer continuous for smooth data (elevation, temperature); use breaks for discrete groups (hazard levels).
A style is classed when steps has more than two break points — the color array then needs one entry per class (N break points produce N − 1 classes; extra colors are ignored). With exactly two steps ([min, max], as below), the style is continuous: the whole color array is interpolated smoothly across the range:
Compute a derived index from multiple bands before classifying. The config.method names the index and maps its inputs to band numbers, and band lists every band used. Supported indices:
NDVI — (NIR − R) / (NIR + R), vegetation health. Requires NIR, R.
NDMI — (NIR − SWIR) / (NIR + SWIR), moisture. Requires NIR, SWIR.
Band numbers are 1-indexed and sensor-specific (Landsat 8/9: R=4, G=3, NIR=5, SWIR=6 · Sentinel-2: R=4, G=3, NIR=8, SWIR=11).
NDVI output ranges from −1 (water/bare) to 1 (dense vegetation); classify it with explicit breaks (e.g. [-1, -0.2, 0, 0.2, 0.4, 0.6, 1]) for labelled classes. Good starting points: NDVI → [-0.5, 0.5] with @cbRedYlGrn; NDWI → [-0.5, 0.25] and NDMI → [-0.5, 0.5] with @feltVibrant.
Raster categories are raw pixel values (integers), so there is no categoricalAttribute. Use {"type": "all"} when you don't know the values, or an explicit array when you do, with one color per value. Don't set rasterResampling here — categorical rasters always use nearest resampling automatically (the property is rejected on this type), since linear interpolation would blend category codes into invalid values.
For raster categorical, use the (@catPalette1…@catPalette7). A fully-transparent class can be expressed as "rgba(0, 0, 0, 0)".
Simulates light and shadow on terrain. Plain hillshade is grayscale relief (config carries just band; paint carries source and intensity). Tinted hillshade overlays a hypsometric color ramp — add steps to config and color to paint; only do this when elevation coloring is wanted.
Tinted, classifying elevation like any numeric raster (use rasterResampling: "linear" and noData for voids):
The Felt SDK enables you to extend Felt maps with custom UI components that integrate seamlessly with the native interface. These extensions allow you to add interactive controls and custom workflows directly within the map experience.
Felt provides two primary ways to add custom UI to your maps:
Action Triggers appear as buttons in the left sidebar and provide quick access to custom actions. Think of them as shortcuts that users can click to trigger specific functionality in your application.
Custom Panels appear in the right sidebar and offer a full canvas for complex UI. These panels can contain forms, controls, and interactive elements that work together to create sophisticated user experiences.
Action triggers are simple button controls that execute custom functions when clicked. They're perfect for actions that don't require additional user input - like applying filters, running calculations, or enabling an interaction mode.
Custom panels provide a structured way to build complex UI within Felt. Each panel consists of three main sections that serve different purposes:
Numeric visualizations use a numeric attribute to either vary colors or sizes between ranges of values and are defined in FSL with "type": "numeric".
Ranges are calculated across discrete steps using a classification method, or on a continuous scale between an attribute’s min and max values. See for the full list (jenks, quantiles, equal-intervals, stddev, geo-intervals, continuous) and how to express steps.
Color numeric values in your data using the color property.
Stepped color
This map shows the percent of renter occupied housing units by US county. Each county is colored according to the step of ranges it falls into using a sequential color palette where light colors are assigned to low values and darker colors for high values.
route
Line
Creates a line that follows the routing logic depending on the mode of transport selected. For instance, walking, driving and cycling routes follow applicable roads and pathways to reach the waypoints the user provides. Flying routes follow great circle paths.
polygon
Polygon
Creates an enclosed area with straight edges
circle
Circle
A circle is defined by its center and radius.
marker
Marker
Freeform drawing with a pen-like rendering. Different sizes can be set for the pen. The geometry produced is in world-space, so as you zoom the map, the pen strokes remain in place.
highlighter
Highlighter
Represents an area of interest, created by drawing with a thick pen. By default, drawing an enclosed shape fills the interior.
text
Text
A label placed on the map with no background color.
note
Note
A label placed on the map with a rectangular background color and either white or black text.
pin
Place
Creates a single point on a map, with a symbol and optional label
line
Line
Creates a sequence of straight lines through the points that the user clicks
categories
Categorical only — {"type": "all"} for every pixel value, or an explicit array.
noData
Value(s) to exclude, e.g. [-9999] for voids or [0] for no-data.
rasterResampling
Numeric and tinted hillshade only — categorical rasters reject it (they always resample nearest). "nearest" keeps exact pixel values; "linear" interpolates (use for continuous imagery/elevation).
outOfRangeValues
"color" clamps to the nearest class color; "hide" makes them transparent.
NDWI — (G − NIR) / (G + NIR), water. Requires G, NIR.
color
—
Raster palette or color array (tinted only), low → high elevation.
simple
Display the raster as-is (satellite, aerial, base tiles)
numeric
Classify one band (elevation, temperature, a hazard index)
numeric
Compute a spectral index (NDVI, NDMI, NDWI) from several bands
categorical
Discrete pixel classes (land cover, soil types)
hillshade
Relief shading from elevation, optionally tinted
band
Which band to read (1-indexed). Numeric/hillshade.
steps
Classification — see Classification methods. {"type": "continuous"} for smooth, or breaks like [0, 500, 1000].
method
source
315
Light azimuth in degrees (0 = North, 90 = East). 315 (northwest) is standard.
intensity
0.5
Spectral-index formula and band mapping (multiband only).
Shadow intensity 0–1; higher = more dramatic relief.
// Configure the line tool
felt.setToolSettings({
tool: "line",
strokeWidth: 8,
color: "#448C2A"
});
// Activate the line tool
felt.setTool("line");
// Later, deactivate the tool
felt.setTool(null);
// Create a polygon
const polygonElement = await felt.createElement({
type: "Polygon",
coordinates: [
[
[-122.42, 37.78],
[-122.41, 37.78],
[-122.41, 37.77],
[-122.42, 37.77],
[-122.42, 37.78]
]
],
color: "#FF5733",
fillOpacity: 0.5
});
// Update its properties
await felt.updateElement({
id: polygonElement.id,
// note that we pass the type here, too in order to get correct
// TypeScript type-checking and autocompletion.
type: "Polygon",
color: "#ABC123",
fillOpacity: 0.5,
strokeWidth: 2
});
// Finally delete the element
await felt.deleteElement(polygonElement.id)// Get an element's geometry in GeoJSON format
const geometry = await felt.getElementGeometry("element-1");
console.log(geometry?.type, geometry?.coordinates);// Set up a listener for changes to a polygon
const unsubscribeChange = felt.onElementChange({
options: { id: polygonElement.id },
handler: ({element}) => {
console.log("Polygon was updated:", element);
}
});
// Set up a listener for deletion
const unsubscribeDelete = felt.onElementDelete({
options: { id: polygonElement.id },
handler: () => {
console.log("Polygon was deleted");
}
});
// Later, clean up listeners
unsubscribeChange();
unsubscribeDelete();// Listen for any element creation
const unsubscribe = felt.onElementCreate({
handler: ({ element, isBeingCreated }) => {
console.log(`New element created with ID: ${element?.id}`);
// Check if the element is still being drawn
if (isBeingCreated) {
console.log("User is still creating this element");
}
}
});
// Or listen for when element creation is completed with a tool
const unsubscribeEnd = felt.onElementCreateEnd({
handler: ({element}) => {
console.log(`Element ${element.id} creation finished`);
}
});
// Later, clean up listeners
unsubscribe();
unsubscribeEnd();// Set your initial tool settings in a style that suits your application
felt.setToolSettings({
tool: "polygon",
strokeWidth: 2,
color: "#FF5733",
fillOpacity: 0.3,
});
// Activate the tool when the user clicks a button in your UI
document.getElementById("polygon-tool").addEventListener("click", () => {
felt.setTool("polygon");
});
// Disable the tool when the user clicks a button in your UI
document.getElementById("reset-tool").addEventListener("click", () => {
felt.setTool(null);
});
// Listen for completed polygons
felt.onElementCreateEnd({
handler: async ({element}) => {
// get the polygon geometry that the user just drew
const geometry = await felt.getElementGeometry(element.id);
// send the polygon to your own backend system
sendToServer(geometry);
}
});{
"version": "2.3.1",
"type": "simple",
"config": {},
"paint": {"opacity": 0.93, "isSandwiched": false}
}{
"version": "2.3.1",
"type": "numeric",
"config": {
"band": 1,
"steps": {"type": "continuous"},
"noData": [-9999],
"rasterResampling": "linear"
},
"paint": {"color": "@mplVirdis", "opacity": 1, "isSandwiched": false},
"legend": {"displayName": "auto"}
}{
"version": "2.3.1",
"type": "numeric",
"config": {"band": 1, "steps": [-154.46, 7987.46]},
"legend": {"displayName": {"0": "-154.46", "1": "7.99K"}},
"paint": {
"opacity": 1,
"isSandwiched": false,
"color": ["#454b9f", "#2d79a4", "#18a2a9", "#8cc187", "#e5d96c", "#eab459", "#ef8b45", "#e66250", "#db2d5e"]
}
}{
"version": "2.3.1",
"type": "numeric",
"config": {
"method": {"NDVI": {"NIR": 5, "R": 4}},
"band": [5, 4],
"steps": {"type": "continuous"},
"noData": [0],
"rasterResampling": "nearest",
"outOfRangeValues": "hide"
},
"paint": {"color": "@cbRedYlGrn", "opacity": 1, "isSandwiched": false},
"legend": {"displayName": "auto"}
}{
"version": "2.3.1",
"type": "categorical",
"config": {"categories": [1, 2, 3, 4, 5]},
"paint": {
"color": ["#228B22", "#4682B4", "#DAA520", "#8B4513", "#808080"],
"opacity": 0.85,
"isSandwiched": true
},
"legend": {
"displayName": {"1": "Forest", "2": "Water", "3": "Cropland", "4": "Bare Soil", "5": "Urban"}
}
}{
"version": "2.3.1",
"type": "hillshade",
"config": {"band": 1},
"paint": {"source": 315, "intensity": 0.5, "isSandwiched": true},
"legend": {}
}{
"version": "2.3.1",
"type": "hillshade",
"config": {
"band": 1,
"steps": {"type": "quantiles", "count": 6},
"noData": [-9999],
"rasterResampling": "linear"
},
"paint": {"color": "@terrain", "source": 315, "intensity": 0.5, "isSandwiched": true},
"legend": {"displayName": "auto"}
}Body - Houses the main interactive elements like forms, selectors, and content areas. This is where users spend most of their time interacting with your custom functionality.
Footer - Typically contains primary action buttons like "Save", "Cancel", or "Apply". This creates a consistent pattern users expect from dialog-style interfaces. The footer sticks to the bottom of the panel, with a divider separating it from the body.
Create a panel by first generating an ID, then specifying its contents. You can control where panels appear using the initialPlacement parameter. When onClickClose is specified, a close button will be rendered in the header.
Custom panels support a variety of interactive and display elements that can be combined to create rich user experiences:
Text elements display formatted content and support full Markdown rendering, allowing you to include headings, lists, links, and formatting within your panels.
TextInput elements allow users to enter custom values like names, descriptions, or numeric parameters.
Control elements allow users to choose from predefined options:
Available control elements include Select, CheckboxGroup, RadioGroup, and ToggleGroup. Each element supports similar properties:
Button elements trigger actions and come in different styles to communicate their importance and effect. Buttons can have different variants (filled, outlined, and transparent) and tints (primary, accent, danger and default):
Primary filled buttons highlight the most important action in a context. Use sparingly - typically one per panel section.
Group related buttons together to create clear action hierarchies:
Button rows automatically handle spacing and alignment, ensuring your panels look polished and consistent.
The grid element helps organize elements within panels to create complex layouts. It uses a grid property that follows the same syntax as the CSS shorthand grid property, and includes verticalAlignment and horizontalDistribution properties for precise control over layout positioning.
iframe elements allow you to embed external content by providing a URL to charts, dashboards, or other web applications directly within your panels.
Divider elements provide visual separation between sections of content in your panels.
A panel is identified by its ID, which must be created using createPanelId. Custom IDs are not supported to prevent conflicts with other panels.
Use createOrUpdatePanel for most panel scenarios. This declarative method lets you specify what the panel should contain, and it handles both creating new panels and updating existing ones with the same API call.
Use updatePanelElements for granular control when you want to modify individual elements. Elements need IDs to be targeted for updates. You can also use createPanelElements to add elements and deletePanelElements to remove elements by their IDs.
await felt.createActionTrigger({
actionTrigger: {
label: "Check solar potential",
onTrigger: async () => {
// Enable polygon tool to allow a user to select a region
await felt.setTool("polygon");
// ...
},
}
});

const panelId = await felt.createPanelId();
await felt.createOrUpdatePanel({
panel: {
id: panelId,
title: "Add report",
body: [
{ type: "Select", placeholder: "Choose a neighborhood", options: [{ label: "Downtown", value: "downtown" }] },
{ type: "Select", placeholder: "Choose a severity", options: [{ label: "Low", value: "low" }, { label: "High", value: "high" }] },
{ type: "TextInput", placeholder: "Email", onBlur: storeEmail },
],
footer: [
{
type: "ButtonRow",
align: "end",
items: [
{ type: "Button", label: "Back", variant: "transparent", tint: "default", onClick: handleBack },
{ type: "Button", label: "Done", variant: "filled", tint: "primary", onClick: handleDone }
]
}
],
onClickClose: async () => {
// Clean up
await felt.deletePanel(panelId);
}
},
initialPlacement: { at: "start" } // Optional: control panel positioning
});{
type: "Text",
content: "**Welcome!** This is a *formatted* text element with [links](https://felt.com).",
}{
type: "TextInput",
placeholder: "First name",
value: "",
onChange: (args) => {
console.log("New value:", args.value);
},
}// Select dropdown
{
type: "Select", // or "CheckboxGroup" | "RadioGroup" | "ToggleGroup"
label: "Year",
options: [
{ value: "2025", label: "2025" },
{ value: "2024", label: "2024" },
],
value: "",
onChange: (args) => {
console.log("Selected:", args.value);
}
}{
type: "Button",
label: "Submit",
variant: "filled", // "transparent" | "outlined"
tint: "primary", // "default" | "accent" | "danger"
onClick: async () => {
// Handle button click
}
}{
type: "ButtonRow",
align: "end",
items: [
{ type: "Button", label: "Clear", variant: "transparent", tint: "default", onClick: handleClear },
{ type: "Button", label: "Send report", variant: "filled", tint: "primary", onClick: handleSend }
]
}{
type: "Grid",
grid: "auto-flow / 2fr 1fr", // CSS grid shorthand
verticalAlignment: "start",
horizontalDistribution: "stretch",
items: [
{ type: "Text", content: "" },
{ type: "Text", content: " \n " },
]
}{
type: "Iframe",
url: "https://example.com/dashboard",
height: 400,
}{
type: "Divider",
}const panelId = await felt.createPanelId();
const greetingElement = { id: "greeting", type: "Text", content: "Hello" };
// Initial state
await felt.createOrUpdatePanel({
panel: {
id: panelId,
title: "My Panel",
body: [greetingElement]
}
});
// Update using destructuring
await felt.createOrUpdatePanel({
panel: {
id: panelId,
title: "My Panel",
body: [{ ...greetingElement, content: "Hello World" }]
}
});const panelId = await felt.createPanelId();
// Create panel with multiple elements
await felt.createOrUpdatePanel({
panel: {
id: panelId,
title: "Data Panel",
body: [
{ id: "status-text", type: "Text", content: "Ready" },
{
id: "layer-select",
type: "Select",
label: "Choose Layer",
options: [
{ value: "layer1", label: "Population" },
{ value: "layer2", label: "Income" }
]
}
]
}
});
// Update only the text element
await felt.updatePanelElements({
panelId,
elements: [{
element: {
id: "status-text",
type: "Text",
content: "Processing data..."
}
}]
});
// Add a new element to the panel
await felt.createPanelElements({
panelId,
elements: [{
element: {
id: "progress-text",
type: "Text",
content: "Progress: 50%"
},
container: "body",
placement: { at: "end" }
}]
});
// Remove an element from the panel
await felt.deletePanelElements({
panelId,
elements: ["progress-text"]
});The style for this map is defined
with the visualization type: numeric
numericAttribute: "Renter occupied (%)" and the computed steps using Jenks Natural Breaks over 5 classes
the sequential @galaxy palette, which Felt expands to one color per class — light colors for low values, dark for high
Continuous Color
The map below shows average accumulated precipitation in the State of California between the years 1900 - 1960 and is colored along a continuous range between the min and max of the precipitation values.
In this case, the numeric style is applied using a continuous method.
In the config block, numericAttribute: PRECIP uses the {"type": "continuous"} steps shortcut, so values scale across the attribute's full min–max range
The color property uses the sequential @purpYl palette, whose colors are interpolated to give each precipitation value a unique color
Numeric color also works on raster datasets, using band instead of numericAttribute — see Raster visualizations.
Size numeric values in your point or line data using the size property.
Stepped size
The map below shows earthquakes over the past year sized with 5 manually defined steps.
In this case, the numeric style is applied using a classed method.
In the config block, the numericAttribute: mag has manually-defined steps for 5 classes
The size property has an array of five sizes, one for each defined class
Map link: Earthquakes Stepped Size
Continuous size
The map below shows tonnes of corn that have been exported from Ukraine since August 2022 under the UN’s Black Sea Grain Initiative. The symbol size for each country is proportionate to its value in the data.
To do this the min and max steps from the numericAttribute: tonnes are interpolated to be proportionately sized between a min and max point size — size:[5,48]
Let Felt compute breaks when you don't have the data in front of you: "steps": {"type": "jenks", "count": 5}. Provide explicit breaks ([0, 3, 5, 7, 9]) when you know the values you want. See Classification methods.
Classed arrays carry one value per class. With N break points you get N − 1 classes, so explicit color/size arrays need N − 1 entries.
Continuous size is written as [min, max] (e.g. [3, 30]) and scales smoothly; proportional-symbol sizes typically range 3–30 px for points. On lines, size is width in pixels (typically 1–20 px), not a radius.
Continuous size legends use inverted indices for historical reasons: "0" labels the maximum value, "1" the midpoint, and "2" the minimum (as in the example above). Continuous color legends run the intuitive way: "0" is the minimum. See .
Icons can vary by value too — set iconImage and let size (or color) carry the numeric variable:
{
"version": "2.3.1",
"type": "numeric",
"config": {
"numericAttribute": "Renter occupied (%)",
"steps": {"type": "jenks", "count": 5}
},
"paint": {
"color": "@galaxy",
"opacity": 0.9,
"strokeColor": "auto",
"strokeWidth": 0.5,
"isSandwiched": true
},
"legend": {"displayName": "auto"}
}{
"version": "2.3.1",
"type": "numeric",
"config": {
"numericAttribute": "PRECIP",
"steps": {"type": "continuous"}
},
"paint": {
"color": "@purpYl",
"opacity": 0.9,
"strokeColor": "auto",
"strokeWidth": 0,
"isClickable": false,
"isSandwiched": true
},
"legend": {"displayName": "auto"}
}{
"version": "2.3.1",
"type": "numeric",
"config": {"numericAttribute": "mag", "steps": [4.5, 5.5, 6, 7, 7.5, 8.2]},
"legend": {"displayName": "auto"},
"paint": {
"size": [5, 10, 12, 14, 16],
"color": "hsl(0, 13%, 45%)",
"opacity": 0.9,
"strokeColor": "hsl(0, 13%, 88%)",
"strokeWidth": 1.5
}
}{
"version": "2.3.1",
"type": "numeric",
"config": {
"steps": [33000, 2344684],
"numericAttribute": "tonnes",
"labelAttribute": ["category"]
},
"label": {
"minZoom": 1,
"color": "#5a5a5a",
"fontSize": 14,
"fontStyle": "Normal",
"fontWeight": 500,
"haloColor": "#d0d0d0",
"haloWidth": 1.5,
"offset": [8, 0]
},
"legend": {"displayName": {"0": "2.34M", "1": "714.65K", "2": "33K"}},
"paint": {
"size": [5, 48],
"color": "hsl(22, 78%, 65%)",
"opacity": 0.9,
"strokeColor": "hsl(22, 78%, 88%)",
"strokeWidth": 1.5
}
}{
"version": "2.3.1",
"type": "numeric",
"config": {"numericAttribute": "beds", "steps": {"type": "jenks", "count": 4}},
"paint": {
"iconImage": "hospital",
"iconFrame": "frame-circle",
"color": "#E74C3C",
"size": [3, 4, 5, 6],
"opacity": 0.9,
"isClickable": true
},
"legend": {"displayName": "auto"}
}Colors appear throughout the Felt Style Language — in paint (color, strokeColor), in label (color, haloColor), and in legends. There are three ways to express a color:
A literal color — a hex, HSL, or RGB string.
A smart color — the keyword "auto", which Felt resolves to a contrasting color at render time.
A palette shortcut — a named, multi-color ramp like @galaxy, used by data-driven (categorical, numeric, heatmap, H3) visualizations.
Any of these string formats are accepted wherever a single color is expected:
rgba(...) is also supported and is handy for fully-transparent fills (for example, a transparent "Other"/background class in a categorical raster: "rgba(0, 0, 0, 0)").
"auto" computes a contrasting color automatically, based on the current map theme and the colors around it. It keeps strokes and label halos legible without hard-coding a value that might clash on a dark basemap or against a particular fill.
Prefer "auto" for:
strokeColor on points and polygons
color and haloColor on labels
…unless a specific color has been requested.
For any data-driven visualization, prefer a named palette over a hand-built color array. Palettes are referenced with an @ prefix (for example "color": "@galaxy") and Felt expands them to the right number of colors for your classes or categories.
Used by categorical, numeric, heatmap, and H3 visualizations on points, lines, and polygons.
Sequential and diverging vector palettes also come in numbered variants that request a specific number of colors — for example @galaxy7, @galaxy8, @galaxy9. Variants exist for 7, 8, and 9 colors (@lightning also has a 6-color variant). The unnumbered name lets Felt pick the right count for you, which is usually what you want.
Used by heatmap visualizations, ordered from least to most density.
Used by numeric, hillshade, and raster-algebra visualizations.
For raster categorical layers, use the vector categorical palettes above (@catPalette1 … @catPalette7, @catPalettePT1, @catPalettePT2).
Raster palettes also support numbered variants to request a specific number of steps — for example @feltGrays2, @feltGrays3, … @feltGrays9.
When a palette or color array drives a data-driven visualization, the number of colors should line up with the number of classes or categories:
Categorical: one color per category. With showOther: true and an explicit categories array, provide N + 1 colors — the extra one styles the "Other" bucket.
Classed numeric: N break points produce N − 1 classes, so an explicit color array needs N − 1 entries.
Continuous: provide 2 or more colors and Felt interpolates smoothly between them in the
A palette shortcut handles all of this for you — Felt expands @galaxy to exactly the colors it needs. Reach for explicit arrays only when you want precise control over each color.









@purpYl
Purple to yellow
@pinkYl
Pink to yellow
@lightning
Lightning gradient
@copper
Copper gradient
@spruce
Spruce green
@riverine
Water gradient
@neptune
Blue gradient
@violet
Violet gradient
@purple
Purple gradient
@grnOr
Green to orange
@purGrn
Purple to green
@weath
Weather gradient
@lightningHeat
Purple → teal → green → yellow
@ylGrnHeat
Dark teal → green → yellow
@bluRdHeat
Blue → gray → red (diverging feel)
@tealRedHeat
Teal → green → yellow → orange → pink
@purpYlPink
Purple → teal → yellow → orange → red/pink
@feltPinks
Pink gradient
@cmOceanGreens
Ocean greens gradient
@pattFeltOcean
Teal ocean gradient
@cmOceanDeep
Deep ocean gradient
@mpaInferno
Matplotlib inferno — note the spelling: mpa, not mpl
@mplPlasma
Matplotlib plasma
@mplVirdis
Matplotlib viridis, perceptually uniform — note the spelling: Virdis, not Viridis
@cividis
Cividis — colorblind-safe
@feltYlRed
Felt yellow to red
@veg
Vegetation gradient
@nclBlOrRed
NCL blue-orange-red diverging
@cbRedYlGrn
ColorBrewer red-yellow-green diverging
@cbBrBG
ColorBrewer brown-blue-green diverging
@wikiTerrain
Wikipedia terrain gradient
@galaxy
Purple to yellow (popular default)
@ylRed
Yellow to red
@ylGrn
@bluRd
Blue to red
@tealOr
Teal to orange (colorblind-safe)
@bluBr
@catPalette1 … @catPalette7
Categorical color schemes
@catPalettePT1
Paul Tol categorical palette 1
@catPalettePT2
@geyser
Green → beige → orange → red (default)
@redOrHeat
Yellow → orange → red → magenta
@purpYlHeat
@feltGrays
Grayscale gradient
@cbBlues
ColorBrewer blues
@cbPurples
@feltWeath
Felt weather gradient
@feltHeat
Felt heat gradient
@feltVibrant
@terrain
Terrain elevation gradient
@rTerrain
R terrain gradient
@feltHypso
@nasaNDVI
NASA NDVI vegetation index
Yellow to green
Blue to brown
Paul Tol categorical palette 2
Light yellow → coral → pink → purple
ColorBrewer purples
Felt vibrant gradient
Felt hypsometric tints
{
"color": "#3B82F6",
"strokeColor": "hsl(217, 80%, 40%)",
"haloColor": "rgb(38, 113, 0)"
}{
"paint": {
"color": "#8F7EBF",
"strokeColor": "auto",
"strokeWidth": 1
},
"label": {
"color": "auto",
"haloColor": "auto",
"haloWidth": 1.5
}
}Color properties (color, strokeColor) accept a literal color or a @palette shortcut, and strokeColor additionally accepts the smart keyword "auto" — see Colors & palettes. Point layers can be drawn as icons via iconImage — see Icons. For data-driven (categorical, numeric, h3) visualizations, paint properties may be arrays, one value per category/class.
Properties common to all visualization types.
The following properties are available for the simple type of visualization
See for the default values of these attributes on each geometry type.
categorical and numeric visualizations use the same color, opacity, size, strokeColor, strokeWidth, and line properties listed above (highlightColor/highlightStrokeColor are simple-only). The difference is that each property may be an array: a single value applies to every category/class, or one value per category/class. See the and pages for worked examples.
When a point layer should be drawn as icons instead of circles, set iconImage in the paint block. The icon takes on the layer's color and size. See for the full catalog and examples.
The paint property can be an array of paint objects for layered rendering. The layers render last-to-first (the last entry is the bottom layer, the first is on top). This is most often used for road-style casing: a wide dark outline beneath a narrower colored fill.
Paint arrays work on all geometries but are most useful for line casing.
Label rendering is configured separately, in the label block. See for the full property reference, defaults, and placement-by-geometry guidance.
These are the properties available to define label rendering. Point and line features are labeled directly; polygon labels are anchored at each polygon's centroid (or along lines with renderAsLines).
In addition, lines support maxAngle (number, default 30), the maximum angle change in degrees between adjacent characters of text following a curved line. Sharper turns aren't placed. See .
See of these attributes on each label type.
placement is shaped differently depending on the geometry being labeled:
Points: an array of compass directions to try, e.g. ["E"] (right of the point), ["E", "W"], or ["Center"]. Felt uses the first placement that doesn't collide.
Lines: a single string relative to the line — "Above", "Center", or "Below". Tune repeatDistance
A feature with a label value can still render no label. Three rules decide:
Collision. A label that overlaps one already placed is dropped rather than moved. padding sets how much space around the text counts as an overlap. Zooming in usually brings the label back.
Line length. Line labels are anchored every repeatDistance pixels and need enough of the run on screen to fit their glyphs. Short segments get nothing. A smaller fontSize or a lower repeatDistance makes room.
If a feature never labels at any zoom, check its value. Null or empty draws nothing.
If using a categorical or numeric visualization, the properties above may be arrays. If there's a single value in the array, that value is used in all categories. If there are as many values as categories, the corresponding value will be used for each category. You can see an example of a categorical viz .
maxAnglePolygons: ["Center"] places the label at the polygon's centroid.
Curvature. maxAngle caps the angle change between adjacent characters, not the overall bend of the line. Sharper turns aren't placed, so tight meanders often go unlabeled at the default 30.
fontFamily
"Atlas Grotesk LC"
"Atlas Grotesk LC"
"Atlas Grotesk LC"
fontSize
13
13
13
fontStyle
"Normal"
"Normal"
"Normal"
fontWeight
500
400
500
haloColor
"#fbfcfb"
"#fbfcfb"
"#fbfcfb"
haloWidth
1
1
1
justify
"auto"
"auto"
"auto"
letterSpacing
0
0
0
lineHeight
1.2
1.2
1.2
maxLineChars
10
-
10
maxAngle
-
30
-
maxZoom
24
24
24
minZoom
24
24
24
offset
[8, 8]
0
-
padding
2
1
0
placement
"auto"
"Above"
"Center"
repeatDistance
-
250
-
textTransform
"none"
"none"
"none"
color
string | auto | Interpolator
Points and lines
Optional. The label color
fontFamily
string
Points and lines
Optional. The font family to use
fontSize
number | Interpolator
Points and lines
Optional. The font size in pixels
fontStyle
"normal" | "italic"
Points and lines
Optional. The font style (case-insensitive). Values other than italic render as normal
fontWeight
number
Points and lines
Optional. The font weight
haloColor
string | Interpolator
Points and lines
Optional. The label halo color
haloWidth
number | Interpolator
Points and lines
Optional. The label halo width in pixels
justify
"auto" | "left" | "right" | "center"
Points and lines
Optional. Text justification for multi-line labels
letterSpacing
number
Points and lines
Optional. Horizontal spacing behaviour between text characters
lineHeight
number
Points and lines
Optional. Sets the height of a line box
maxLineChars
number | Interpolator
Points and lines
Optional. Defines the max number of characters before a line break
maxZoom
number
Points and lines
Optional. The maximum zoom level at which the label will be shown. Defaults to 24
minZoom
number
Points and lines
Optional. The minimum zoom level at which the label will be shown. Defaults to 24 (see note below)
offset
[number, number] | number
Points and lines
Optional. In the case of points, this value must be an array of two numeric offsets that will be applied on the positive X and Y axis defined by the label placement (i.e. an offset of [3,4] with a label placement of NE moves the label 3pixels to the right and 4 pixels above of the anchor point. An offset of [3,4] with a label placement of SW moves the label 3pixels to the left and 4 pixels below of the anchor point). In case of lines, this value is a single number that moves the label following the label position (i.e. an offset of 3 with a label position of Above will move the label 3 pixels above the line following the line normal. An offset of 3 with a label position of Below will mode the label 3 pixels under the line following the line normal)
padding
number
Points and lines
Optional. Adds invisible padding around the label that's used to compute label collisions
placement
string[] | "auto" | string
Points and lines
Optional. On points: an array of placements to try ("N", "NE", "E", "SE", "S", "SW", "W", "NW", "Center") or "auto"; if all placements collide with existing labels, the label is not shown. On lines: a single placement relative to the line — "Above", "Center" or "Below"
renderAsLines
boolean
Polygons
Optional. Renders labels along lines instead of using the centroids
repeatDistance
number
Lines
Optional. The distance in pixels between label repetitions on a line
textTransform
"none" | "uppercase" | "lowercase"
Points and lines
Optional. Specifies how to capitalize the label
isClickable
boolean
Points, Lines and Polygons
Optional. A flag to tell if labels should be clickable
isHoverable
boolean
Points, Lines and Polygons
Optional. A flag to tell if labels should be hoverable
color
"#333333"
"#333333"
"#333333"
boolean
false
Optional. A flag to tell if features should be hoverable
isSandwiched
boolean
false
Optional. A flag to tell if features affected by this visualization need to be rendered below the basemap road and water layers. Only applies to polygon features, point and line features are already rendered on top of the basemap
maxZoom
number
24
Optional. The maximum zoom level at which the visualization will be shown
minZoom
number
0
Optional. The minimum zoom level at which the visualization will be shown
renderAsLines
boolean
false
Optional. Decides if a polygon dataset should be rendered as lines thus making them render above the basemap. Note that using this requires that the style uses line properties instead of polygon ones.
paintPropertyOverrides
object
Optional. An escape hatch: raw paint or layout properties applied to the generated layer. Keys that are not valid for the underlying MapLibre layer type are silently ignored. Also available on the label block. See for more.
number[]
Lines
Optional. The dash line definition — an array of dash/gap lengths with an even number of entries, e.g. [2, 1]
highlightColor
string
Points, lines and polygons
Optional. The color to be used when a feature is selected
highlightStrokeColor
string
Points, lines and polygons
Optional. The stroke color to be used when a feature is selected
highlightStrokeWidth
number |
Points and polygons
Optional. The stroke width when a feature is selected
lineCap
"butt" | "round" | "square"|
Lines
Optional. The shape used to draw the end points of lines
lineJoin
"bevel" | "round"| "miter"|
Lines
Optional. The shape used to join two line segments when they meet
opacity
number |
Points, lines and polygons
Optional. The opacity to use from 0 to 1
size
number |
Points and lines
Optional. Point radius or line width in pixels
strokeColor
string | | auto
Points and polygons
Optional. Stroke color
strokeWidth
number |
Points and polygons
Optional. Stroke width in pixels
iconRotation
number | string
Rotation in degrees, or a column name to rotate by.
iconHideOnZoom
number
Zoom level below which icons render as plain points.
"#EA3891"
"#EA3891"
"#EA3891"
highlightStrokeColor
"#EA3891"
"#EA3891"
"#EA3891"
dashArray
-
-
lineCap
-
-
"round"
lineJoin
-
-
"round"
opacity
0.9
0.8
1
isSandwiched
-
false
-
size
4
-
2
strokeColor
"#F9F8Fb"
"#777777"
-
strokeWidth
1
1
-
isClickable
boolean
true
Optional. A flag to tell if features should be clickable
color
string | Interpolator
Points, lines and polygons
Optional. The color to be used
iconImage
string | string[]
An icon slug, or an emoji as "emoji::name:". Array form assigns an icon per category/class.
iconFrame
"none" | "frame-circle" | "frame-square"
{
"version": "2.3.1",
"type": "simple",
"paint": [
{"color": "#3B82F6", "size": 4, "opacity": 1.0, "lineCap": "round", "lineJoin": "round"},
{"color": "#1E3A5F", "size": 8, "opacity": 1.0, "lineCap": "round", "lineJoin": "round"}
],
"legend": {}
}color
"#EE4D5A"
"#826DBA"
"#4CC8A3"
isHoverable
dashArray
A frame drawn behind built-in icons (not emojis).
highlightColor
Retrieve profile information and settings for the authenticated user.
User
luCHyMruTQ6ozGk3gPJfEBUnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
GET /api/v2/user HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
"email": "text",
"id": "luCHyMruTQ6ozGk3gPJfEB",
"name": "text"
}Retrieve all projects accessible to the authenticated user within the workspace.
Only needed when using the API as part of a plugin
Projects
luCHyMruTQ6ozGk3gPJfEBhttps://felt.com/api/v2/projects/V0dnOMOuTd9B9BOsL9C0UjmqCThe maximum permission level workspace members inherit on team-visible projects.
view_onlyPossible values: UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
GET /api/v2/projects HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
[
{
"id": "luCHyMruTQ6ozGk3gPJfEB",
"links": {
"self": "https://felt.com/api/v2/projects/V0dnOMOuTd9B9BOsL9C0UjmqC"
},
"max_inherited_permission": "view_only",
"name": "text",
"type": "project_reference",
"visibility": "workspace"
}
]Create a new project with specified name and visibility settings within the workspace.
The maximum permission level workspace members inherit on team-visible projects. Only applicable when visibility is "workspace".
view_onlyPossible values: The name to be used for the Project
Either viewable by all members of the workspace, or private to users who are invited.
Project
luCHyMruTQ6ozGk3gPJfEB2024-05-25T15:51:34luCHyMruTQ6ozGk3gPJfEBhttps://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqCA static thumbnail image of the map
The maximum permission level workspace members inherit on team-visible projects.
view_onlyPossible values: UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
POST /api/v2/projects HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 79
{
"max_inherited_permission": "view_only",
"name": "text",
"visibility": "workspace"
}{
"id": "luCHyMruTQ6ozGk3gPJfEB",
"maps": [
{
"created_at": "2024-05-25T15:51:34",
"folder_id": "text",
"id": "luCHyMruTQ6ozGk3gPJfEB",
"links": {
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC"
},
"project_id": "text",
"public_access": "private",
"thumbnail_url": "text",
"title": "text",
"type": "map_reference",
"url": "text",
"visited_at": "text"
}
],
"max_inherited_permission": "view_only",
"name": "text",
"type": "project",
"visibility": "workspace"
}Retrieve detailed information about a specific project including metadata, permissions, and references to the maps in the project.
Project
luCHyMruTQ6ozGk3gPJfEB2024-05-25T15:51:34luCHyMruTQ6ozGk3gPJfEBhttps://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqCA static thumbnail image of the map
The maximum permission level workspace members inherit on team-visible projects.
view_onlyPossible values: UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
GET /api/v2/projects/{project_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
"id": "luCHyMruTQ6ozGk3gPJfEB",
"maps": [
{
"created_at": "2024-05-25T15:51:34",
"folder_id": "text",
"id": "luCHyMruTQ6ozGk3gPJfEB",
"links": {
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC"
},
"project_id": "text",
"public_access": "private",
"thumbnail_url": "text",
"title": "text",
"type": "map_reference",
"url": "text",
"visited_at": "text"
}
],
"max_inherited_permission": "view_only",
"name": "text",
"type": "project",
"visibility": "workspace"
}Permanently delete a project and all its contained maps and folders.
The ID of the Project to delete. Note: This will delete all Folders and Maps inside the project!
No Content
No content
UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
DELETE /api/v2/projects/{project_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
No content
Update project properties including name and visibility settings.
The ID of the project to update
The maximum permission level workspace members inherit on team-visible projects. Only applicable when visibility is "workspace".
view_onlyPossible values: The name to be used for the Project
Either viewable by all members of the workspace, or private to users who are invited.
Project
luCHyMruTQ6ozGk3gPJfEB2024-05-25T15:51:34luCHyMruTQ6ozGk3gPJfEBhttps://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqCA static thumbnail image of the map
The maximum permission level workspace members inherit on team-visible projects.
view_onlyPossible values: UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
POST /api/v2/projects/{project_id}/update HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 79
{
"max_inherited_permission": "view_only",
"name": "text",
"visibility": "workspace"
}{
"id": "luCHyMruTQ6ozGk3gPJfEB",
"maps": [
{
"created_at": "2024-05-25T15:51:34",
"folder_id": "text",
"id": "luCHyMruTQ6ozGk3gPJfEB",
"links": {
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC"
},
"project_id": "text",
"public_access": "private",
"thumbnail_url": "text",
"title": "text",
"type": "map_reference",
"url": "text",
"visited_at": "text"
}
],
"max_inherited_permission": "view_only",
"name": "text",
"type": "project",
"visibility": "workspace"
}Returns all components on a layer, ordered as they appear in the app.
The ID of the map the layer belongs to.
The ID of the layer the components belong to.
Layer components list
Whether this component reacts to other components' selections, filtering its result to match.
trueCompute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: Count features.
numbrojs format object. See the numbrojs format documentation.
The statistic to compute over aggregate_by.
Attribute to aggregate.
numbrojs format object. See the numbrojs format documentation.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueSelected values of data.group_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: Count features.
numbrojs format object. See the numbrojs format documentation.
Numeric attribute to bin into the distribution.
Number of equal-width bins across the attribute's range.
The statistic to compute over aggregate_by.
Attribute to aggregate.
numbrojs format object. See the numbrojs format documentation.
Numeric attribute to bin into the distribution.
Number of equal-width bins across the attribute's range.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueSelected values of data.domain. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: Count features.
Numeric attribute the line is plotted against — the horizontal axis.
numbrojs format object. See the numbrojs format documentation.
The statistic to compute over aggregate_by.
Attribute to aggregate.
Numeric attribute the line is plotted against — the horizontal axis.
numbrojs format object. See the numbrojs format documentation.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueSelected values of data.group_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: Count features.
numbrojs format object. See the numbrojs format documentation.
Attribute whose values become the categories.
The statistic to compute over aggregate_by.
Attribute to aggregate.
numbrojs format object. See the numbrojs format documentation.
Attribute whose values become the categories.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueSelected values of data.group_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: Count features.
numbrojs format object. See the numbrojs format documentation.
Date or datetime attribute which drives the time axis.
The statistic to compute over aggregate_by.
Attribute to aggregate.
numbrojs format object. See the numbrojs format documentation.
Date or datetime attribute which drives the time axis.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
The type of rendered interactive component. slider only works with numeric attributes.
dropdownPossible values: Whether the dropdown allows filtering on multiple values or just one.
Selected values of data.filter_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
The rendered order of dropdown options.
Attribute to filter values from.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title. Defaults to data.filter_by.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueCompute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: The statistic to compute over aggregate_by.
A band id such as band:1 or the styled index band index:0.
numbrojs format object. See the numbrojs format documentation.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueSelected values of data.group_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: numbrojs format object. See the numbrojs format documentation.
A band id such as band:1 or the styled index band index:0.
Number of equal-width bins across the attribute's range.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueSelected values of data.domain. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: The band whose pixels are charted.
numbrojs format object. See the numbrojs format documentation.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueSelected values of data.group_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: A band id such as band:1 or the styled index band index:0.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
The type of rendered interactive component. A dropdown lists the band's values, so it needs a categorical band. Defaults to the band's control: dropdown for a categorical band, slider otherwise.
Whether the dropdown allows filtering on multiple values or just one.
Selected values of data.filter_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
The rendered order of dropdown options.
A band id such as band:1.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title. Defaults to the band's name.
The component type. Immutable after creation.
UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
GET /api/v2/maps/{map_id}/layers/{layer_id}/components HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
[
{
"config": {
"reactive": true,
"viewport_mode": "global"
},
"data": {
"aggregate": "count",
"format": null
},
"id": "text",
"layer_id": "text",
"layer_type": "vector",
"title": "text",
"type": "statistic"
}
]Creates a component on a layer.
The ID of the map the layer belongs to.
The ID of the layer the component belongs to.
Whether this component reacts to other components' selections, filtering its result to match.
trueCompute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: How the statistic is computed.
Count features.
numbrojs format object. See the numbrojs format documentation.
The statistic to compute over aggregate_by.
Attribute to aggregate.
numbrojs format object. See the numbrojs format documentation.
The kind of layer this component is attached to.
vectorPossible values: Display title.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueSelected values of data.group_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: How to compute the histogram distribution.
Count features.
numbrojs format object. See the numbrojs format documentation.
Numeric attribute to bin into the distribution.
Number of equal-width bins across the attribute's range.
The statistic to compute over aggregate_by.
Attribute to aggregate.
numbrojs format object. See the numbrojs format documentation.
Numeric attribute to bin into the distribution.
Number of equal-width bins across the attribute's range.
The kind of layer this component is attached to.
vectorPossible values: Display title.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueSelected values of data.domain. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: How to compute the line's distribution.
Count features.
Numeric attribute the line is plotted against — the horizontal axis.
numbrojs format object. See the numbrojs format documentation.
The statistic to compute over aggregate_by.
Attribute to aggregate.
Numeric attribute the line is plotted against — the horizontal axis.
numbrojs format object. See the numbrojs format documentation.
The kind of layer this component is attached to.
vectorPossible values: Display title.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueSelected values of data.group_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: How to compute the resulting categories.
Count features.
numbrojs format object. See the numbrojs format documentation.
Attribute whose values become the categories.
The statistic to compute over aggregate_by.
Attribute to aggregate.
numbrojs format object. See the numbrojs format documentation.
Attribute whose values become the categories.
The kind of layer this component is attached to.
vectorPossible values: Display title.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueSelected values of data.group_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: How to aggregate the chosen attribute into a time series.
Count features.
numbrojs format object. See the numbrojs format documentation.
Date or datetime attribute which drives the time axis.
The statistic to compute over aggregate_by.
Attribute to aggregate.
numbrojs format object. See the numbrojs format documentation.
Date or datetime attribute which drives the time axis.
The kind of layer this component is attached to.
vectorPossible values: Display title.
The component type. Immutable after creation.
The type of rendered interactive component. slider only works with numeric attributes.
dropdownPossible values: Whether the dropdown allows filtering on multiple values or just one.
Selected values of data.filter_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
The rendered order of dropdown options.
Attribute to filter values from.
The kind of layer this component is attached to.
vectorPossible values: Display title. Defaults to data.filter_by.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueCompute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: The statistic to compute over aggregate_by.
A band id such as band:1 or the styled index band index:0.
numbrojs format object. See the numbrojs format documentation.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueSelected values of data.group_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: numbrojs format object. See the numbrojs format documentation.
A band id such as band:1 or the styled index band index:0.
Number of equal-width bins across the attribute's range.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueSelected values of data.domain. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: The band whose pixels are charted.
numbrojs format object. See the numbrojs format documentation.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueSelected values of data.group_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: A band id such as band:1 or the styled index band index:0.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
The type of rendered interactive component. A dropdown lists the band's values, so it needs a categorical band. Defaults to the band's control: dropdown for a categorical band, slider otherwise.
Whether the dropdown allows filtering on multiple values or just one.
Selected values of data.filter_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
The rendered order of dropdown options.
A band id such as band:1.
The kind of layer this component is attached to.
Display title. Defaults to the band's name.
The component type. Immutable after creation.
Layer component
Whether this component reacts to other components' selections, filtering its result to match.
trueCompute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: Count features.
numbrojs format object. See the numbrojs format documentation.
The statistic to compute over aggregate_by.
Attribute to aggregate.
numbrojs format object. See the numbrojs format documentation.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueSelected values of data.group_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: Count features.
numbrojs format object. See the numbrojs format documentation.
Numeric attribute to bin into the distribution.
Number of equal-width bins across the attribute's range.
The statistic to compute over aggregate_by.
Attribute to aggregate.
numbrojs format object. See the numbrojs format documentation.
Numeric attribute to bin into the distribution.
Number of equal-width bins across the attribute's range.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueSelected values of data.domain. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: Count features.
Numeric attribute the line is plotted against — the horizontal axis.
numbrojs format object. See the numbrojs format documentation.
The statistic to compute over aggregate_by.
Attribute to aggregate.
Numeric attribute the line is plotted against — the horizontal axis.
numbrojs format object. See the numbrojs format documentation.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueSelected values of data.group_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: Count features.
numbrojs format object. See the numbrojs format documentation.
Attribute whose values become the categories.
The statistic to compute over aggregate_by.
Attribute to aggregate.
numbrojs format object. See the numbrojs format documentation.
Attribute whose values become the categories.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueSelected values of data.group_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: Count features.
numbrojs format object. See the numbrojs format documentation.
Date or datetime attribute which drives the time axis.
The statistic to compute over aggregate_by.
Attribute to aggregate.
numbrojs format object. See the numbrojs format documentation.
Date or datetime attribute which drives the time axis.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
The type of rendered interactive component. slider only works with numeric attributes.
dropdownPossible values: Whether the dropdown allows filtering on multiple values or just one.
Selected values of data.filter_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
The rendered order of dropdown options.
Attribute to filter values from.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title. Defaults to data.filter_by.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueCompute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: The statistic to compute over aggregate_by.
A band id such as band:1 or the styled index band index:0.
numbrojs format object. See the numbrojs format documentation.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueSelected values of data.group_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: numbrojs format object. See the numbrojs format documentation.
A band id such as band:1 or the styled index band index:0.
Number of equal-width bins across the attribute's range.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueSelected values of data.domain. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: The band whose pixels are charted.
numbrojs format object. See the numbrojs format documentation.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueSelected values of data.group_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: A band id such as band:1 or the styled index band index:0.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
The type of rendered interactive component. A dropdown lists the band's values, so it needs a categorical band. Defaults to the band's control: dropdown for a categorical band, slider otherwise.
Whether the dropdown allows filtering on multiple values or just one.
Selected values of data.filter_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
The rendered order of dropdown options.
A band id such as band:1.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title. Defaults to the band's name.
The component type. Immutable after creation.
UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
POST /api/v2/maps/{map_id}/layers/{layer_id}/components HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 215
{
"config": {
"selection": [
"zoning",
"in",
[
"residential"
]
]
},
"data": {
"aggregate": "sum",
"aggregate_by": "area_sqm",
"format": {
"thousandSeparated": true
},
"group_by": "zoning"
},
"title": "Total area by zoning",
"type": "bar_chart"
}{
"config": {
"reactive": true,
"viewport_mode": "global"
},
"data": {
"aggregate": "count",
"format": null
},
"id": "text",
"layer_id": "text",
"layer_type": "vector",
"title": "text",
"type": "statistic"
}Returns a single layer component. Includes all fields.
The ID of the map the layer belongs to.
The ID of the layer the component belongs to.
The ID of the layer component.
Layer component
Whether this component reacts to other components' selections, filtering its result to match.
trueCompute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: Count features.
numbrojs format object. See the numbrojs format documentation.
The statistic to compute over aggregate_by.
Attribute to aggregate.
numbrojs format object. See the numbrojs format documentation.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueSelected values of data.group_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: Count features.
numbrojs format object. See the numbrojs format documentation.
Numeric attribute to bin into the distribution.
Number of equal-width bins across the attribute's range.
The statistic to compute over aggregate_by.
Attribute to aggregate.
numbrojs format object. See the numbrojs format documentation.
Numeric attribute to bin into the distribution.
Number of equal-width bins across the attribute's range.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueSelected values of data.domain. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: Count features.
Numeric attribute the line is plotted against — the horizontal axis.
numbrojs format object. See the numbrojs format documentation.
The statistic to compute over aggregate_by.
Attribute to aggregate.
Numeric attribute the line is plotted against — the horizontal axis.
numbrojs format object. See the numbrojs format documentation.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueSelected values of data.group_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: Count features.
numbrojs format object. See the numbrojs format documentation.
Attribute whose values become the categories.
The statistic to compute over aggregate_by.
Attribute to aggregate.
numbrojs format object. See the numbrojs format documentation.
Attribute whose values become the categories.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueSelected values of data.group_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: Count features.
numbrojs format object. See the numbrojs format documentation.
Date or datetime attribute which drives the time axis.
The statistic to compute over aggregate_by.
Attribute to aggregate.
numbrojs format object. See the numbrojs format documentation.
Date or datetime attribute which drives the time axis.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
The type of rendered interactive component. slider only works with numeric attributes.
dropdownPossible values: Whether the dropdown allows filtering on multiple values or just one.
Selected values of data.filter_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
The rendered order of dropdown options.
Attribute to filter values from.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title. Defaults to data.filter_by.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueCompute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: The statistic to compute over aggregate_by.
A band id such as band:1 or the styled index band index:0.
numbrojs format object. See the numbrojs format documentation.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueSelected values of data.group_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: numbrojs format object. See the numbrojs format documentation.
A band id such as band:1 or the styled index band index:0.
Number of equal-width bins across the attribute's range.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueSelected values of data.domain. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: The band whose pixels are charted.
numbrojs format object. See the numbrojs format documentation.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueSelected values of data.group_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: A band id such as band:1 or the styled index band index:0.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
The type of rendered interactive component. A dropdown lists the band's values, so it needs a categorical band. Defaults to the band's control: dropdown for a categorical band, slider otherwise.
Whether the dropdown allows filtering on multiple values or just one.
Selected values of data.filter_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
The rendered order of dropdown options.
A band id such as band:1.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title. Defaults to the band's name.
The component type. Immutable after creation.
UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
GET /api/v2/maps/{map_id}/layers/{layer_id}/components/{component_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
"config": {
"reactive": true,
"viewport_mode": "global"
},
"data": {
"aggregate": "count",
"format": null
},
"id": "text",
"layer_id": "text",
"layer_type": "vector",
"title": "text",
"type": "statistic"
}Partially update the provided fields of a component.
The ID of the map the layer belongs to.
The ID of the layer the component belongs to.
The ID of the layer component.
Whether this component reacts to other components' selections, filtering its result to match.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
Change how the component computes its result. data replaces as a unit when provided. Partial updates are not supported.
Count features.
numbrojs format object. See the numbrojs format documentation.
The statistic to compute over aggregate_by.
Attribute to aggregate.
numbrojs format object. See the numbrojs format documentation.
The kind of layer this component is attached to.
vectorPossible values: Display title.
Whether this component reacts to other components' selections, filtering its result to match.
Selected values of data.group_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block. Pass null to clear the selection.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
Change how the component computes its result. data replaces as a unit when provided. Partial updates are not supported.
Count features.
numbrojs format object. See the numbrojs format documentation.
Numeric attribute to bin into the distribution.
Number of equal-width bins across the attribute's range.
The statistic to compute over aggregate_by.
Attribute to aggregate.
numbrojs format object. See the numbrojs format documentation.
Numeric attribute to bin into the distribution.
Number of equal-width bins across the attribute's range.
The kind of layer this component is attached to.
vectorPossible values: Display title.
Whether this component reacts to other components' selections, filtering its result to match.
Selected values of data.domain. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block. Pass null to clear the selection.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
Change how the component computes its result. data replaces as a unit when provided. Partial updates are not supported.
Count features.
Numeric attribute the line is plotted against — the horizontal axis.
numbrojs format object. See the numbrojs format documentation.
The statistic to compute over aggregate_by.
Attribute to aggregate.
Numeric attribute the line is plotted against — the horizontal axis.
numbrojs format object. See the numbrojs format documentation.
The kind of layer this component is attached to.
vectorPossible values: Display title.
Whether this component reacts to other components' selections, filtering its result to match.
Selected values of data.group_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block. Pass null to clear the selection.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
Change how the component computes its result. data replaces as a unit when provided. Partial updates are not supported.
Count features.
numbrojs format object. See the numbrojs format documentation.
Attribute whose values become the categories.
The statistic to compute over aggregate_by.
Attribute to aggregate.
numbrojs format object. See the numbrojs format documentation.
Attribute whose values become the categories.
The kind of layer this component is attached to.
vectorPossible values: Display title.
Whether this component reacts to other components' selections, filtering its result to match.
Selected values of data.group_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block. Pass null to clear the selection.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
Change how the component computes its result. data replaces as a unit when provided. Partial updates are not supported.
Count features.
numbrojs format object. See the numbrojs format documentation.
Date or datetime attribute which drives the time axis.
The statistic to compute over aggregate_by.
Attribute to aggregate.
numbrojs format object. See the numbrojs format documentation.
Date or datetime attribute which drives the time axis.
The kind of layer this component is attached to.
vectorPossible values: Display title.
The type of rendered interactive component. slider only works with numeric attributes.
Whether the dropdown allows filtering on multiple values or just one.
Selected values of data.filter_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block. Pass null to clear the selection.
The rendered order of dropdown options.
Attribute to filter values from.
The kind of layer this component is attached to.
vectorPossible values: Display title.
Whether this component reacts to other components' selections, filtering its result to match.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
The statistic to compute over aggregate_by.
A band id such as band:1 or the styled index band index:0.
numbrojs format object. See the numbrojs format documentation.
The kind of layer this component is attached to.
Display title.
Whether this component reacts to other components' selections, filtering its result to match.
Selected values of data.group_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block. Pass null to clear the selection.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
numbrojs format object. See the numbrojs format documentation.
A band id such as band:1 or the styled index band index:0.
Number of equal-width bins across the attribute's range.
The kind of layer this component is attached to.
Display title.
Whether this component reacts to other components' selections, filtering its result to match.
Selected values of data.domain. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block. Pass null to clear the selection.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
The band whose pixels are charted.
numbrojs format object. See the numbrojs format documentation.
The kind of layer this component is attached to.
Display title.
Whether this component reacts to other components' selections, filtering its result to match.
Selected values of data.group_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block. Pass null to clear the selection.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
A band id such as band:1 or the styled index band index:0.
The kind of layer this component is attached to.
Display title.
The type of rendered interactive component. A dropdown lists the band's values, so it needs a categorical band.
Whether the dropdown allows filtering on multiple values or just one.
Selected values of data.filter_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block. Pass null to clear the selection.
The rendered order of dropdown options.
A band id such as band:1.
The kind of layer this component is attached to.
Display title.
Layer component
Whether this component reacts to other components' selections, filtering its result to match.
trueCompute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: Count features.
numbrojs format object. See the numbrojs format documentation.
The statistic to compute over aggregate_by.
Attribute to aggregate.
numbrojs format object. See the numbrojs format documentation.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueSelected values of data.group_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: Count features.
numbrojs format object. See the numbrojs format documentation.
Numeric attribute to bin into the distribution.
Number of equal-width bins across the attribute's range.
The statistic to compute over aggregate_by.
Attribute to aggregate.
numbrojs format object. See the numbrojs format documentation.
Numeric attribute to bin into the distribution.
Number of equal-width bins across the attribute's range.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueSelected values of data.domain. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: Count features.
Numeric attribute the line is plotted against — the horizontal axis.
numbrojs format object. See the numbrojs format documentation.
The statistic to compute over aggregate_by.
Attribute to aggregate.
Numeric attribute the line is plotted against — the horizontal axis.
numbrojs format object. See the numbrojs format documentation.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueSelected values of data.group_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: Count features.
numbrojs format object. See the numbrojs format documentation.
Attribute whose values become the categories.
The statistic to compute over aggregate_by.
Attribute to aggregate.
numbrojs format object. See the numbrojs format documentation.
Attribute whose values become the categories.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueSelected values of data.group_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: Count features.
numbrojs format object. See the numbrojs format documentation.
Date or datetime attribute which drives the time axis.
The statistic to compute over aggregate_by.
Attribute to aggregate.
numbrojs format object. See the numbrojs format documentation.
Date or datetime attribute which drives the time axis.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
The type of rendered interactive component. slider only works with numeric attributes.
dropdownPossible values: Whether the dropdown allows filtering on multiple values or just one.
Selected values of data.filter_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
The rendered order of dropdown options.
Attribute to filter values from.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title. Defaults to data.filter_by.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueCompute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: The statistic to compute over aggregate_by.
A band id such as band:1 or the styled index band index:0.
numbrojs format object. See the numbrojs format documentation.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueSelected values of data.group_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: numbrojs format object. See the numbrojs format documentation.
A band id such as band:1 or the styled index band index:0.
Number of equal-width bins across the attribute's range.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueSelected values of data.domain. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: The band whose pixels are charted.
numbrojs format object. See the numbrojs format documentation.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
Whether this component reacts to other components' selections, filtering its result to match.
trueSelected values of data.group_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
Compute over the whole layer (global) or only the features in the current viewport (viewport).
globalPossible values: A band id such as band:1 or the styled index band index:0.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title.
The component type. Immutable after creation.
The type of rendered interactive component. A dropdown lists the band's values, so it needs a categorical band. Defaults to the band's control: dropdown for a categorical band, slider otherwise.
Whether the dropdown allows filtering on multiple values or just one.
Selected values of data.filter_by. The visualized map layer and other components are filtered to match. Uses the same format as the FSL filters block.
The rendered order of dropdown options.
A band id such as band:1.
Server-assigned component id.
The layer this component is bound to.
The kind of layer this component is attached to.
Display title. Defaults to the band's name.
The component type. Immutable after creation.
UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
POST /api/v2/maps/{map_id}/layers/{layer_id}/components/{component_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 133
{
"config": {
"reactive": true,
"viewport_mode": "global"
},
"data": {
"aggregate": "count",
"format": null
},
"layer_type": "vector",
"title": "text"
}{
"config": {
"reactive": true,
"viewport_mode": "global"
},
"data": {
"aggregate": "count",
"format": null
},
"id": "text",
"layer_id": "text",
"layer_type": "vector",
"title": "text",
"type": "statistic"
}Deletes a component from the layer.
The ID of the map the layer belongs to.
The ID of the layer the component belongs to.
The ID of the layer component.
No Content
No content
UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
DELETE /api/v2/maps/{map_id}/layers/{layer_id}/components/{component_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
No content
Retrieve detailed information about a specific layer group including its layers and configuration.
Layer Group
luCHyMruTQ6ozGk3gPJfEBThe name of the attribute
The type of the attribute
luCHyMruTQ6ozGk3gPJfEBISO 8601 timestamp of when the layer's data was last updated. This includes scheduled refreshes, manual refreshes, and direct feature edits.
Controls how the layer is displayed in the legend.
Controls whether or not the layer is displayed in the legend. Defaults to "show".
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/componentshttps://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA2025-03-24ISO 8601 timestamp of when the next scheduled refresh will occur. Null if refresh is disabled or paused.
A sort order key used for ordering layers and layer groups in the legend
Why the layer's refresh is paused. Null when not paused.
Whether scheduled refresh is active, paused (due to failures), or disabled
The Felt Style Language style for the layer
Deprecated: use caption instead.
The tile URL for this layer
Controls how the layer group is displayed in the legend. Defaults to "show".
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6DA sort order key used for ordering layers and layer groups in the legend
Deprecated: use caption instead.
Controls how the layer group is displayed in the legend. Defaults to "default".
UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
GET /api/v2/maps/{map_id}/layer_groups/{layer_group_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
"caption": "text",
"id": "luCHyMruTQ6ozGk3gPJfEB",
"layers": [
{
"attributes": [
{
"name": "text",
"type": "INTEGER"
}
],
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"last_refreshed_at": "2026-01-01T00:00:00.000Z",
"legend_display": "default",
"legend_visibility": "hide",
"links": {
"components": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/components",
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA"
},
"metadata": {
"attribution_text": "text",
"attribution_url": "text",
"description": "text",
"license": "text",
"source_abbreviation": "text",
"source_name": "text",
"source_url": "text",
"updated_at": "2025-03-24"
},
"name": "text",
"next_refresh_at": "2026-01-01T00:00:00.000Z",
"ordering_key": 1,
"paused_reason": "consecutive_failures",
"progress": 1,
"refresh_period": "15 min",
"refresh_status": "active",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
],
"legend_visibility": "hide",
"links": {
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6D"
},
"name": "text",
"ordering_key": 1,
"type": "layer_group",
"visibility_interaction": "default"
}Update layer group properties including name, visibility, and organization settings.
A very interesting groupluCHyMruTQ6ozGk3gPJfEBControls how the layer group is displayed in the legend
My Layer GroupDeprecated: use caption instead.
Controls how the layer group is displayed in the legend. Defaults to "default".
LayerGroup
luCHyMruTQ6ozGk3gPJfEBThe name of the attribute
The type of the attribute
luCHyMruTQ6ozGk3gPJfEBISO 8601 timestamp of when the layer's data was last updated. This includes scheduled refreshes, manual refreshes, and direct feature edits.
Controls how the layer is displayed in the legend.
Controls whether or not the layer is displayed in the legend. Defaults to "show".
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/componentshttps://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA2025-03-24ISO 8601 timestamp of when the next scheduled refresh will occur. Null if refresh is disabled or paused.
A sort order key used for ordering layers and layer groups in the legend
Why the layer's refresh is paused. Null when not paused.
Whether scheduled refresh is active, paused (due to failures), or disabled
The Felt Style Language style for the layer
Deprecated: use caption instead.
The tile URL for this layer
Controls how the layer group is displayed in the legend. Defaults to "show".
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6DA sort order key used for ordering layers and layer groups in the legend
Deprecated: use caption instead.
Controls how the layer group is displayed in the legend. Defaults to "default".
UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
POST /api/v2/maps/{map_id}/layer_groups/{layer_group_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 171
{
"caption": "A very interesting group",
"id": "luCHyMruTQ6ozGk3gPJfEB",
"legend_visibility": "hide",
"name": "My Layer Group",
"ordering_key": 1,
"visibility_interaction": "default"
}{
"caption": "text",
"id": "luCHyMruTQ6ozGk3gPJfEB",
"layers": [
{
"attributes": [
{
"name": "text",
"type": "INTEGER"
}
],
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"last_refreshed_at": "2026-01-01T00:00:00.000Z",
"legend_display": "default",
"legend_visibility": "hide",
"links": {
"components": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/components",
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA"
},
"metadata": {
"attribution_text": "text",
"attribution_url": "text",
"description": "text",
"license": "text",
"source_abbreviation": "text",
"source_name": "text",
"source_url": "text",
"updated_at": "2025-03-24"
},
"name": "text",
"next_refresh_at": "2026-01-01T00:00:00.000Z",
"ordering_key": 1,
"paused_reason": "consecutive_failures",
"progress": 1,
"refresh_period": "15 min",
"refresh_status": "active",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
],
"legend_visibility": "hide",
"links": {
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6D"
},
"name": "text",
"ordering_key": 1,
"type": "layer_group",
"visibility_interaction": "default"
}Permanently remove a layer group and all its contained layers from a map.
The ID of the map to delete the layer group from
The ID of the layer group to delete
No Content
No content
UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
DELETE /api/v2/maps/{map_id}/layer_groups/{layer_group_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
No content
Retrieve all layers from a map, including uploaded files and connected data sources.
Every layer you have created is listed, so a layer whose upload has not finished comes back with "status": "uploading" even though the map does not show it yet — an upload that never arrives can be cleaned up by deleting the layer. Pass include_uploading=false for just the layers the map shows.
Set to false to leave out the layers whose upload has not finished, matching what the map shows.
trueLayers list
The name of the attribute
The type of the attribute
luCHyMruTQ6ozGk3gPJfEBISO 8601 timestamp of when the layer's data was last updated. This includes scheduled refreshes, manual refreshes, and direct feature edits.
Controls how the layer is displayed in the legend.
Controls whether or not the layer is displayed in the legend. Defaults to "show".
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/componentshttps://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA2025-03-24ISO 8601 timestamp of when the next scheduled refresh will occur. Null if refresh is disabled or paused.
A sort order key used for ordering layers and layer groups in the legend
Why the layer's refresh is paused. Null when not paused.
Whether scheduled refresh is active, paused (due to failures), or disabled
The Felt Style Language style for the layer
Deprecated: use caption instead.
The tile URL for this layer
UnauthorizedError
NotFoundError
InternalServerError
GET /api/v2/maps/{map_id}/layers HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
[
{
"attributes": [
{
"name": "text",
"type": "INTEGER"
}
],
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"last_refreshed_at": "2026-01-01T00:00:00.000Z",
"legend_display": "default",
"legend_visibility": "hide",
"links": {
"components": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/components",
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA"
},
"metadata": {
"attribution_text": "text",
"attribution_url": "text",
"description": "text",
"license": "text",
"source_abbreviation": "text",
"source_name": "text",
"source_url": "text",
"updated_at": "2025-03-24"
},
"name": "text",
"next_refresh_at": "2026-01-01T00:00:00.000Z",
"ordering_key": 1,
"paused_reason": "consecutive_failures",
"progress": 1,
"refresh_period": "15 min",
"refresh_status": "active",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
]Update layer properties including styling, visibility, grouping, and other configuration options.
A very interesting datasetluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBControls how the layer is displayed in the legend.
Controls whether or not the layer is displayed in the legend.
2025-03-24My LayerDeprecated: use caption instead.
Layer list
The name of the attribute
The type of the attribute
luCHyMruTQ6ozGk3gPJfEBISO 8601 timestamp of when the layer's data was last updated. This includes scheduled refreshes, manual refreshes, and direct feature edits.
Controls how the layer is displayed in the legend.
Controls whether or not the layer is displayed in the legend. Defaults to "show".
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/componentshttps://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA2025-03-24ISO 8601 timestamp of when the next scheduled refresh will occur. Null if refresh is disabled or paused.
A sort order key used for ordering layers and layer groups in the legend
Why the layer's refresh is paused. Null when not paused.
Whether scheduled refresh is active, paused (due to failures), or disabled
The Felt Style Language style for the layer
Deprecated: use caption instead.
The tile URL for this layer
UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
POST /api/v2/maps/{map_id}/layers HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 427
[
{
"caption": "A very interesting dataset",
"id": "luCHyMruTQ6ozGk3gPJfEB",
"layer_group_id": "luCHyMruTQ6ozGk3gPJfEB",
"legend_display": "default",
"legend_visibility": "hide",
"metadata": {
"attribution_text": "text",
"attribution_url": "text",
"description": "text",
"license": "text",
"source_abbreviation": "text",
"source_name": "text",
"source_url": "text",
"updated_at": "2025-03-24"
},
"name": "My Layer",
"ordering_key": 1,
"refresh_period": "15 min"
}
][
{
"attributes": [
{
"name": "text",
"type": "INTEGER"
}
],
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"last_refreshed_at": "2026-01-01T00:00:00.000Z",
"legend_display": "default",
"legend_visibility": "hide",
"links": {
"components": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/components",
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA"
},
"metadata": {
"attribution_text": "text",
"attribution_url": "text",
"description": "text",
"license": "text",
"source_abbreviation": "text",
"source_name": "text",
"source_url": "text",
"updated_at": "2025-03-24"
},
"name": "text",
"next_refresh_at": "2026-01-01T00:00:00.000Z",
"ordering_key": 1,
"paused_reason": "consecutive_failures",
"progress": 1,
"refresh_period": "15 min",
"refresh_status": "active",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
]Retrieve all layer groups from a map to see how layers are organized.
Every layer group you have created is listed, including one whose upload has not finished and which the map does not show yet. Pass include_uploading=false for just the layer groups the map shows.
Set to false to leave out the layer groups whose upload has not finished, matching what the map shows.
trueLayers Groups
luCHyMruTQ6ozGk3gPJfEBThe name of the attribute
The type of the attribute
luCHyMruTQ6ozGk3gPJfEBISO 8601 timestamp of when the layer's data was last updated. This includes scheduled refreshes, manual refreshes, and direct feature edits.
Controls how the layer is displayed in the legend.
Controls whether or not the layer is displayed in the legend. Defaults to "show".
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/componentshttps://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA2025-03-24ISO 8601 timestamp of when the next scheduled refresh will occur. Null if refresh is disabled or paused.
A sort order key used for ordering layers and layer groups in the legend
Why the layer's refresh is paused. Null when not paused.
Whether scheduled refresh is active, paused (due to failures), or disabled
The Felt Style Language style for the layer
Deprecated: use caption instead.
The tile URL for this layer
Controls how the layer group is displayed in the legend. Defaults to "show".
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6DA sort order key used for ordering layers and layer groups in the legend
Deprecated: use caption instead.
Controls how the layer group is displayed in the legend. Defaults to "default".
UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
GET /api/v2/maps/{map_id}/layer_groups HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
[
{
"caption": "text",
"id": "luCHyMruTQ6ozGk3gPJfEB",
"layers": [
{
"attributes": [
{
"name": "text",
"type": "INTEGER"
}
],
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"last_refreshed_at": "2026-01-01T00:00:00.000Z",
"legend_display": "default",
"legend_visibility": "hide",
"links": {
"components": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/components",
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA"
},
"metadata": {
"attribution_text": "text",
"attribution_url": "text",
"description": "text",
"license": "text",
"source_abbreviation": "text",
"source_name": "text",
"source_url": "text",
"updated_at": "2025-03-24"
},
"name": "text",
"next_refresh_at": "2026-01-01T00:00:00.000Z",
"ordering_key": 1,
"paused_reason": "consecutive_failures",
"progress": 1,
"refresh_period": "15 min",
"refresh_status": "active",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
],
"legend_visibility": "hide",
"links": {
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6D"
},
"name": "text",
"ordering_key": 1,
"type": "layer_group",
"visibility_interaction": "default"
}
]Update properties for multiple layer groups in a single request for efficient bulk operations.
A very interesting groupluCHyMruTQ6ozGk3gPJfEBControls how the layer group is displayed in the legend
My Layer GroupDeprecated: use caption instead.
Controls how the layer group is displayed in the legend. Defaults to "default".
LayerGroup list
luCHyMruTQ6ozGk3gPJfEBThe name of the attribute
The type of the attribute
luCHyMruTQ6ozGk3gPJfEBISO 8601 timestamp of when the layer's data was last updated. This includes scheduled refreshes, manual refreshes, and direct feature edits.
Controls how the layer is displayed in the legend.
Controls whether or not the layer is displayed in the legend. Defaults to "show".
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/componentshttps://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA2025-03-24ISO 8601 timestamp of when the next scheduled refresh will occur. Null if refresh is disabled or paused.
A sort order key used for ordering layers and layer groups in the legend
Why the layer's refresh is paused. Null when not paused.
Whether scheduled refresh is active, paused (due to failures), or disabled
The Felt Style Language style for the layer
Deprecated: use caption instead.
The tile URL for this layer
Controls how the layer group is displayed in the legend. Defaults to "show".
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6DA sort order key used for ordering layers and layer groups in the legend
Deprecated: use caption instead.
Controls how the layer group is displayed in the legend. Defaults to "default".
UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
POST /api/v2/maps/{map_id}/layer_groups HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 173
[
{
"caption": "A very interesting group",
"id": "luCHyMruTQ6ozGk3gPJfEB",
"legend_visibility": "hide",
"name": "My Layer Group",
"ordering_key": 1,
"visibility_interaction": "default"
}
][
{
"caption": "text",
"id": "luCHyMruTQ6ozGk3gPJfEB",
"layers": [
{
"attributes": [
{
"name": "text",
"type": "INTEGER"
}
],
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"last_refreshed_at": "2026-01-01T00:00:00.000Z",
"legend_display": "default",
"legend_visibility": "hide",
"links": {
"components": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/components",
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA"
},
"metadata": {
"attribution_text": "text",
"attribution_url": "text",
"description": "text",
"license": "text",
"source_abbreviation": "text",
"source_name": "text",
"source_url": "text",
"updated_at": "2025-03-24"
},
"name": "text",
"next_refresh_at": "2026-01-01T00:00:00.000Z",
"ordering_key": 1,
"paused_reason": "consecutive_failures",
"progress": 1,
"refresh_period": "15 min",
"refresh_status": "active",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
],
"legend_visibility": "hide",
"links": {
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6D"
},
"name": "text",
"ordering_key": 1,
"type": "layer_group",
"visibility_interaction": "default"
}
]Update the visual styling properties of a layer including colors, symbols, and rendering options.
The ID of the map where the layer is located
The ID of the layer to update the style of
The new layer style, specified in Felt Style Language format
Layer
The name of the attribute
The type of the attribute
luCHyMruTQ6ozGk3gPJfEBISO 8601 timestamp of when the layer's data was last updated. This includes scheduled refreshes, manual refreshes, and direct feature edits.
Controls how the layer is displayed in the legend.
Controls whether or not the layer is displayed in the legend. Defaults to "show".
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/componentshttps://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA2025-03-24ISO 8601 timestamp of when the next scheduled refresh will occur. Null if refresh is disabled or paused.
A sort order key used for ordering layers and layer groups in the legend
Why the layer's refresh is paused. Null when not paused.
Whether scheduled refresh is active, paused (due to failures), or disabled
The Felt Style Language style for the layer
Deprecated: use caption instead.
The tile URL for this layer
UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
POST /api/v2/maps/{map_id}/layers/{layer_id}/update_style HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 12
{
"style": {}
}{
"attributes": [
{
"name": "text",
"type": "INTEGER"
}
],
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"last_refreshed_at": "2026-01-01T00:00:00.000Z",
"legend_display": "default",
"legend_visibility": "hide",
"links": {
"components": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/components",
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA"
},
"metadata": {
"attribution_text": "text",
"attribution_url": "text",
"description": "text",
"license": "text",
"source_abbreviation": "text",
"source_name": "text",
"source_url": "text",
"updated_at": "2025-03-24"
},
"name": "text",
"next_refresh_at": "2026-01-01T00:00:00.000Z",
"ordering_key": 1,
"paused_reason": "consecutive_failures",
"progress": 1,
"refresh_period": "15 min",
"refresh_status": "active",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}Retrieve detailed information about a specific layer including data source, styling, and configuration.
Layer
The name of the attribute
The type of the attribute
luCHyMruTQ6ozGk3gPJfEBISO 8601 timestamp of when the layer's data was last updated. This includes scheduled refreshes, manual refreshes, and direct feature edits.
Controls how the layer is displayed in the legend.
Controls whether or not the layer is displayed in the legend. Defaults to "show".
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/componentshttps://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA2025-03-24ISO 8601 timestamp of when the next scheduled refresh will occur. Null if refresh is disabled or paused.
A sort order key used for ordering layers and layer groups in the legend
Why the layer's refresh is paused. Null when not paused.
Whether scheduled refresh is active, paused (due to failures), or disabled
The Felt Style Language style for the layer
Deprecated: use caption instead.
The tile URL for this layer
UnauthorizedError
NotFoundError
InternalServerError
GET /api/v2/maps/{map_id}/layers/{layer_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
"attributes": [
{
"name": "text",
"type": "INTEGER"
}
],
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"last_refreshed_at": "2026-01-01T00:00:00.000Z",
"legend_display": "default",
"legend_visibility": "hide",
"links": {
"components": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/components",
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA"
},
"metadata": {
"attribution_text": "text",
"attribution_url": "text",
"description": "text",
"license": "text",
"source_abbreviation": "text",
"source_name": "text",
"source_url": "text",
"updated_at": "2025-03-24"
},
"name": "text",
"next_refresh_at": "2026-01-01T00:00:00.000Z",
"ordering_key": 1,
"paused_reason": "consecutive_failures",
"progress": 1,
"refresh_period": "15 min",
"refresh_status": "active",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}Permanently remove a layer from a map.
The ID of the map to delete the layer from
The ID of the layer to delete
No Content
No content
UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
DELETE /api/v2/maps/{map_id}/layers/{layer_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
No content
Copy layers or layer groups to other maps, preserving styling and configuration.
luCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBDuplicate Layers Response
luCHyMruTQ6ozGk3gPJfEBThe name of the attribute
The type of the attribute
luCHyMruTQ6ozGk3gPJfEBISO 8601 timestamp of when the layer's data was last updated. This includes scheduled refreshes, manual refreshes, and direct feature edits.
Controls how the layer is displayed in the legend.
Controls whether or not the layer is displayed in the legend. Defaults to "show".
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/componentshttps://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA2025-03-24ISO 8601 timestamp of when the next scheduled refresh will occur. Null if refresh is disabled or paused.
A sort order key used for ordering layers and layer groups in the legend
Why the layer's refresh is paused. Null when not paused.
Whether scheduled refresh is active, paused (due to failures), or disabled
The Felt Style Language style for the layer
Deprecated: use caption instead.
The tile URL for this layer
Controls how the layer group is displayed in the legend. Defaults to "show".
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6DA sort order key used for ordering layers and layer groups in the legend
Deprecated: use caption instead.
Controls how the layer group is displayed in the legend. Defaults to "default".
The name of the attribute
The type of the attribute
luCHyMruTQ6ozGk3gPJfEBISO 8601 timestamp of when the layer's data was last updated. This includes scheduled refreshes, manual refreshes, and direct feature edits.
Controls how the layer is displayed in the legend.
Controls whether or not the layer is displayed in the legend. Defaults to "show".
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/componentshttps://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA2025-03-24ISO 8601 timestamp of when the next scheduled refresh will occur. Null if refresh is disabled or paused.
A sort order key used for ordering layers and layer groups in the legend
Why the layer's refresh is paused. Null when not paused.
Whether scheduled refresh is active, paused (due to failures), or disabled
The Felt Style Language style for the layer
Deprecated: use caption instead.
The tile URL for this layer
UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
POST /api/v2/duplicate_layers HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 92
[
{
"destination_map_id": "luCHyMruTQ6ozGk3gPJfEB",
"source_layer_id": "luCHyMruTQ6ozGk3gPJfEB"
}
]{
"layer_groups": [
{
"caption": "text",
"id": "luCHyMruTQ6ozGk3gPJfEB",
"layers": [
{
"attributes": [
{
"name": "text",
"type": "INTEGER"
}
],
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"last_refreshed_at": "2026-01-01T00:00:00.000Z",
"legend_display": "default",
"legend_visibility": "hide",
"links": {
"components": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/components",
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA"
},
"metadata": {
"attribution_text": "text",
"attribution_url": "text",
"description": "text",
"license": "text",
"source_abbreviation": "text",
"source_name": "text",
"source_url": "text",
"updated_at": "2025-03-24"
},
"name": "text",
"next_refresh_at": "2026-01-01T00:00:00.000Z",
"ordering_key": 1,
"paused_reason": "consecutive_failures",
"progress": 1,
"refresh_period": "15 min",
"refresh_status": "active",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
],
"legend_visibility": "hide",
"links": {
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6D"
},
"name": "text",
"ordering_key": 1,
"type": "layer_group",
"visibility_interaction": "default"
}
],
"layers": [
{
"attributes": [
{
"name": "text",
"type": "INTEGER"
}
],
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"last_refreshed_at": "2026-01-01T00:00:00.000Z",
"legend_display": "default",
"legend_visibility": "hide",
"links": {
"components": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/components",
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA"
},
"metadata": {
"attribution_text": "text",
"attribution_url": "text",
"description": "text",
"license": "text",
"source_abbreviation": "text",
"source_name": "text",
"source_url": "text",
"updated_at": "2025-03-24"
},
"name": "text",
"next_refresh_at": "2026-01-01T00:00:00.000Z",
"ordering_key": 1,
"paused_reason": "consecutive_failures",
"progress": 1,
"refresh_period": "15 min",
"refresh_status": "active",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
]
}Create a new layer from an existing data source connection (database, API, or file).
luCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBAddSourceLayerAccepted
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/KFFhKAbvS4anD3wxtwNEpDhttps://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqCUnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
POST /api/v2/maps/{map_id}/add_source_layer HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 56
{
"dataset_id": "luCHyMruTQ6ozGk3gPJfEB",
"from": "dataset"
}{
"links": {
"layer_group": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/KFFhKAbvS4anD3wxtwNEpD",
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC"
},
"status": "accepted"
}Trigger a data refresh for a layer from its original data source to pull in the latest updates.
After uploading a file or URL, you may want to update the resulting layer with new data. The process is quite similar to the upload:
For URL uploads, simply making a single POST request to the refresh endpoint is enough
For file refreshes, the response of the initial POST request will include a URL and some pre-signed attributes, which will be used to upload the new file to Amazon S3.
The ID of the map hosting the layer to refresh
The ID of the layer to refresh
Refresh response
luCHyMruTQ6ozGk3gPJfEBThe ID of the layer created by this upload. If multiple layers are included in the upload, this is the ID of the first layer in the layer group.
luCHyMruTQ6ozGk3gPJfEBIf provided, the presigned attributes to attach to the post request
If provided, the URL to post the file to
UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
POST /api/v2/maps/{map_id}/layers/{layer_id}/refresh HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
"layer_group_id": "luCHyMruTQ6ozGk3gPJfEB",
"layer_id": "luCHyMruTQ6ozGk3gPJfEB",
"presigned_attributes": {},
"type": "upload_response",
"url": "text"
}Upload a file or import data from a URL to create a new layer on the map.
The /upload endpoint can be used for both URL and file uploads:
For URL uploads, simply making a single POST request to the upload endpoint is enough
For file uploads, the response of the initial POST request contain information you will use to upload the file to Amazon S3
Check our docs to see what URLs are supported.
The ID of the map to upload the layer to.
ex: USA
ex: Oakland
ex: 94612
ex: California
ex: 1904 Franklin St
ex: USA
ex: Oakland
ex: California
A public URL containing geodata to import, in place of uploading a file.
(Image uploads only) The latitude of the image center.
(Image uploads only) The longitude of the image center.
2025-03-24The display name for the new layer.
(Image uploads only) The zoom level of the image.
Upload layer response
luCHyMruTQ6ozGk3gPJfEBThe ID of the layer created by this upload. If multiple layers are included in the upload, this is the ID of the first layer in the layer group.
luCHyMruTQ6ozGk3gPJfEBIf provided, the presigned attributes to attach to the post request
If provided, the URL to post the file to
UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
POST /api/v2/maps/{map_id}/upload HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 311
{
"hints": [
{
"attributes": {
"lat": "text",
"lng": "text"
}
}
],
"import_url": "text",
"lat": 1,
"lng": 1,
"metadata": {
"attribution_text": "text",
"attribution_url": "text",
"description": "text",
"license": "text",
"source_abbreviation": "text",
"source_name": "text",
"source_url": "text",
"updated_at": "2025-03-24"
},
"name": "text",
"zoom": 1
}{
"layer_group_id": "luCHyMruTQ6ozGk3gPJfEB",
"layer_id": "luCHyMruTQ6ozGk3gPJfEB",
"presigned_attributes": {},
"type": "upload_response",
"url": "text"
}Move a map to a different project or folder within the same workspace. Project IDs and Folder IDs can be found inside map settings.
luCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBMap
2024-05-25T15:51:34luCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBThe name of the attribute
The type of the attribute
luCHyMruTQ6ozGk3gPJfEBISO 8601 timestamp of when the layer's data was last updated. This includes scheduled refreshes, manual refreshes, and direct feature edits.
Controls how the layer is displayed in the legend.
Controls whether or not the layer is displayed in the legend. Defaults to "show".
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/componentshttps://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA2025-03-24ISO 8601 timestamp of when the next scheduled refresh will occur. Null if refresh is disabled or paused.
A sort order key used for ordering layers and layer groups in the legend
Why the layer's refresh is paused. Null when not paused.
Whether scheduled refresh is active, paused (due to failures), or disabled
The Felt Style Language style for the layer
Deprecated: use caption instead.
The tile URL for this layer
Controls how the layer group is displayed in the legend. Defaults to "show".
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6DA sort order key used for ordering layers and layer groups in the legend
Deprecated: use caption instead.
Controls how the layer group is displayed in the legend. Defaults to "default".
The name of the attribute
The type of the attribute
luCHyMruTQ6ozGk3gPJfEBISO 8601 timestamp of when the layer's data was last updated. This includes scheduled refreshes, manual refreshes, and direct feature edits.
Controls how the layer is displayed in the legend.
Controls whether or not the layer is displayed in the legend. Defaults to "show".
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/componentshttps://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA2025-03-24ISO 8601 timestamp of when the next scheduled refresh will occur. Null if refresh is disabled or paused.
A sort order key used for ordering layers and layer groups in the legend
Why the layer's refresh is paused. Null when not paused.
Whether scheduled refresh is active, paused (due to failures), or disabled
The Felt Style Language style for the layer
Deprecated: use caption instead.
The tile URL for this layer
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqCluCHyMruTQ6ozGk3gPJfEBWhether viewers can open the data table
A static thumbnail image of the map
Whether viewers can duplicate the map and data
Whether viewers can export map data
Whether viewers can see who else is viewing the map
UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
POST /api/v2/maps/{map_id}/move HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 39
{
"project_id": "luCHyMruTQ6ozGk3gPJfEB"
}{
"basemap": "text",
"created_at": "2024-05-25T15:51:34",
"element_groups": [
{
"elements": {
"features": [
{
"geometry": {
"felt:id": "luCHyMruTQ6ozGk3gPJfEB",
"felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
},
"properties": {},
"type": "Feature"
}
],
"type": "FeatureCollection"
},
"id": "luCHyMruTQ6ozGk3gPJfEB",
"name": "text"
}
],
"elements": {
"features": [
{
"geometry": {
"felt:id": "luCHyMruTQ6ozGk3gPJfEB",
"felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
},
"properties": {},
"type": "Feature"
}
],
"type": "FeatureCollection"
},
"folder_id": "text",
"id": "luCHyMruTQ6ozGk3gPJfEB",
"layer_groups": [
{
"caption": "text",
"id": "luCHyMruTQ6ozGk3gPJfEB",
"layers": [
{
"attributes": [
{
"name": "text",
"type": "INTEGER"
}
],
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"last_refreshed_at": "2026-01-01T00:00:00.000Z",
"legend_display": "default",
"legend_visibility": "hide",
"links": {
"components": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/components",
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA"
},
"metadata": {
"attribution_text": "text",
"attribution_url": "text",
"description": "text",
"license": "text",
"source_abbreviation": "text",
"source_name": "text",
"source_url": "text",
"updated_at": "2025-03-24"
},
"name": "text",
"next_refresh_at": "2026-01-01T00:00:00.000Z",
"ordering_key": 1,
"paused_reason": "consecutive_failures",
"progress": 1,
"refresh_period": "15 min",
"refresh_status": "active",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
],
"legend_visibility": "hide",
"links": {
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6D"
},
"name": "text",
"ordering_key": 1,
"type": "layer_group",
"visibility_interaction": "default"
}
],
"layers": [
{
"attributes": [
{
"name": "text",
"type": "INTEGER"
}
],
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"last_refreshed_at": "2026-01-01T00:00:00.000Z",
"legend_display": "default",
"legend_visibility": "hide",
"links": {
"components": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/components",
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA"
},
"metadata": {
"attribution_text": "text",
"attribution_url": "text",
"description": "text",
"license": "text",
"source_abbreviation": "text",
"source_name": "text",
"source_url": "text",
"updated_at": "2025-03-24"
},
"name": "text",
"next_refresh_at": "2026-01-01T00:00:00.000Z",
"ordering_key": 1,
"paused_reason": "consecutive_failures",
"progress": 1,
"refresh_period": "15 min",
"refresh_status": "active",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
],
"links": {
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC"
},
"project_id": "text",
"public_access": "private",
"table_settings": {
"default_table_layer_id": "luCHyMruTQ6ozGk3gPJfEB",
"viewers_can_open_table": true
},
"thumbnail_url": "text",
"title": "text",
"type": "map",
"url": "text",
"viewer_permissions": {
"can_duplicate_map": true,
"can_export_data": true,
"can_see_map_presence": true
},
"visited_at": "text"
}Create a copy of a map with all its layers, elements, and configuration.
The ID of the map to duplicate
luCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBTitle for the duplicated map. If not provided, will default to '[Original Title] (copy)'
Duplicated Map
2024-05-25T15:51:34luCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBThe name of the attribute
The type of the attribute
luCHyMruTQ6ozGk3gPJfEBISO 8601 timestamp of when the layer's data was last updated. This includes scheduled refreshes, manual refreshes, and direct feature edits.
Controls how the layer is displayed in the legend.
Controls whether or not the layer is displayed in the legend. Defaults to "show".
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/componentshttps://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA2025-03-24ISO 8601 timestamp of when the next scheduled refresh will occur. Null if refresh is disabled or paused.
A sort order key used for ordering layers and layer groups in the legend
Why the layer's refresh is paused. Null when not paused.
Whether scheduled refresh is active, paused (due to failures), or disabled
The Felt Style Language style for the layer
Deprecated: use caption instead.
The tile URL for this layer
Controls how the layer group is displayed in the legend. Defaults to "show".
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6DA sort order key used for ordering layers and layer groups in the legend
Deprecated: use caption instead.
Controls how the layer group is displayed in the legend. Defaults to "default".
The name of the attribute
The type of the attribute
luCHyMruTQ6ozGk3gPJfEBISO 8601 timestamp of when the layer's data was last updated. This includes scheduled refreshes, manual refreshes, and direct feature edits.
Controls how the layer is displayed in the legend.
Controls whether or not the layer is displayed in the legend. Defaults to "show".
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/componentshttps://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA2025-03-24ISO 8601 timestamp of when the next scheduled refresh will occur. Null if refresh is disabled or paused.
A sort order key used for ordering layers and layer groups in the legend
Why the layer's refresh is paused. Null when not paused.
Whether scheduled refresh is active, paused (due to failures), or disabled
The Felt Style Language style for the layer
Deprecated: use caption instead.
The tile URL for this layer
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqCluCHyMruTQ6ozGk3gPJfEBWhether viewers can open the data table
A static thumbnail image of the map
Whether viewers can duplicate the map and data
Whether viewers can export map data
Whether viewers can see who else is viewing the map
UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
POST /api/v2/maps/{map_id}/duplicate HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 70
{
"destination": {
"project_id": "luCHyMruTQ6ozGk3gPJfEB"
},
"title": "text"
}{
"basemap": "text",
"created_at": "2024-05-25T15:51:34",
"element_groups": [
{
"elements": {
"features": [
{
"geometry": {
"felt:id": "luCHyMruTQ6ozGk3gPJfEB",
"felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
},
"properties": {},
"type": "Feature"
}
],
"type": "FeatureCollection"
},
"id": "luCHyMruTQ6ozGk3gPJfEB",
"name": "text"
}
],
"elements": {
"features": [
{
"geometry": {
"felt:id": "luCHyMruTQ6ozGk3gPJfEB",
"felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
},
"properties": {},
"type": "Feature"
}
],
"type": "FeatureCollection"
},
"folder_id": "text",
"id": "luCHyMruTQ6ozGk3gPJfEB",
"layer_groups": [
{
"caption": "text",
"id": "luCHyMruTQ6ozGk3gPJfEB",
"layers": [
{
"attributes": [
{
"name": "text",
"type": "INTEGER"
}
],
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"last_refreshed_at": "2026-01-01T00:00:00.000Z",
"legend_display": "default",
"legend_visibility": "hide",
"links": {
"components": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/components",
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA"
},
"metadata": {
"attribution_text": "text",
"attribution_url": "text",
"description": "text",
"license": "text",
"source_abbreviation": "text",
"source_name": "text",
"source_url": "text",
"updated_at": "2025-03-24"
},
"name": "text",
"next_refresh_at": "2026-01-01T00:00:00.000Z",
"ordering_key": 1,
"paused_reason": "consecutive_failures",
"progress": 1,
"refresh_period": "15 min",
"refresh_status": "active",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
],
"legend_visibility": "hide",
"links": {
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6D"
},
"name": "text",
"ordering_key": 1,
"type": "layer_group",
"visibility_interaction": "default"
}
],
"layers": [
{
"attributes": [
{
"name": "text",
"type": "INTEGER"
}
],
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"last_refreshed_at": "2026-01-01T00:00:00.000Z",
"legend_display": "default",
"legend_visibility": "hide",
"links": {
"components": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/components",
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA"
},
"metadata": {
"attribution_text": "text",
"attribution_url": "text",
"description": "text",
"license": "text",
"source_abbreviation": "text",
"source_name": "text",
"source_url": "text",
"updated_at": "2025-03-24"
},
"name": "text",
"next_refresh_at": "2026-01-01T00:00:00.000Z",
"ordering_key": 1,
"paused_reason": "consecutive_failures",
"progress": 1,
"refresh_period": "15 min",
"refresh_status": "active",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
],
"links": {
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC"
},
"project_id": "text",
"public_access": "private",
"table_settings": {
"default_table_layer_id": "luCHyMruTQ6ozGk3gPJfEB",
"viewers_can_open_table": true
},
"thumbnail_url": "text",
"title": "text",
"type": "map",
"url": "text",
"viewer_permissions": {
"can_duplicate_map": true,
"can_export_data": true,
"can_see_map_presence": true
},
"visited_at": "text"
}Create a new map with optional customization options.
Several aspects can be customized when creating a new map, including:
Title
Initial location (latitude, longitude and zoom level)
Sharing permissions (defaults to viewing and commenting for users with the map URL)
The basemap to use for the new map. Defaults to "default". Valid values are "default", "light", "dark", "satellite", a valid raster tile URL with {x}, {y}, and {z} parameters, or a hex color string like #ff0000.
A description to display in the map legend
If no data has been uploaded to the map, the initial latitude to center the map display on.
An array of urls to use to create layers in the map. Only tile URLs for raster layers are supported at the moment.
If no data has been uploaded to the map, the initial longitude to center the map display on.
The level of access to grant to the map. Defaults to "view_only".
The title to be used for the map. Defaults to "Untitled Map"
The workspace to create the map in. Defaults to the latest used workspace
If no data has been uploaded to the map, the initial zoom level for the map to display.
Map
2024-05-25T15:51:34luCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBThe name of the attribute
The type of the attribute
luCHyMruTQ6ozGk3gPJfEBISO 8601 timestamp of when the layer's data was last updated. This includes scheduled refreshes, manual refreshes, and direct feature edits.
Controls how the layer is displayed in the legend.
Controls whether or not the layer is displayed in the legend. Defaults to "show".
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/componentshttps://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA2025-03-24ISO 8601 timestamp of when the next scheduled refresh will occur. Null if refresh is disabled or paused.
A sort order key used for ordering layers and layer groups in the legend
Why the layer's refresh is paused. Null when not paused.
Whether scheduled refresh is active, paused (due to failures), or disabled
The Felt Style Language style for the layer
Deprecated: use caption instead.
The tile URL for this layer
Controls how the layer group is displayed in the legend. Defaults to "show".
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6DA sort order key used for ordering layers and layer groups in the legend
Deprecated: use caption instead.
Controls how the layer group is displayed in the legend. Defaults to "default".
The name of the attribute
The type of the attribute
luCHyMruTQ6ozGk3gPJfEBISO 8601 timestamp of when the layer's data was last updated. This includes scheduled refreshes, manual refreshes, and direct feature edits.
Controls how the layer is displayed in the legend.
Controls whether or not the layer is displayed in the legend. Defaults to "show".
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/componentshttps://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA2025-03-24ISO 8601 timestamp of when the next scheduled refresh will occur. Null if refresh is disabled or paused.
A sort order key used for ordering layers and layer groups in the legend
Why the layer's refresh is paused. Null when not paused.
Whether scheduled refresh is active, paused (due to failures), or disabled
The Felt Style Language style for the layer
Deprecated: use caption instead.
The tile URL for this layer
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqCluCHyMruTQ6ozGk3gPJfEBWhether viewers can open the data table
A static thumbnail image of the map
Whether viewers can duplicate the map and data
Whether viewers can export map data
Whether viewers can see who else is viewing the map
UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
POST /api/v2/maps HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 149
{
"basemap": "text",
"description": "text",
"lat": 1,
"layer_urls": [
"text"
],
"lon": 1,
"public_access": "private",
"title": "text",
"workspace_id": "text",
"zoom": 1
}{
"basemap": "text",
"created_at": "2024-05-25T15:51:34",
"element_groups": [
{
"elements": {
"features": [
{
"geometry": {
"felt:id": "luCHyMruTQ6ozGk3gPJfEB",
"felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
},
"properties": {},
"type": "Feature"
}
],
"type": "FeatureCollection"
},
"id": "luCHyMruTQ6ozGk3gPJfEB",
"name": "text"
}
],
"elements": {
"features": [
{
"geometry": {
"felt:id": "luCHyMruTQ6ozGk3gPJfEB",
"felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
},
"properties": {},
"type": "Feature"
}
],
"type": "FeatureCollection"
},
"folder_id": "text",
"id": "luCHyMruTQ6ozGk3gPJfEB",
"layer_groups": [
{
"caption": "text",
"id": "luCHyMruTQ6ozGk3gPJfEB",
"layers": [
{
"attributes": [
{
"name": "text",
"type": "INTEGER"
}
],
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"last_refreshed_at": "2026-01-01T00:00:00.000Z",
"legend_display": "default",
"legend_visibility": "hide",
"links": {
"components": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/components",
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA"
},
"metadata": {
"attribution_text": "text",
"attribution_url": "text",
"description": "text",
"license": "text",
"source_abbreviation": "text",
"source_name": "text",
"source_url": "text",
"updated_at": "2025-03-24"
},
"name": "text",
"next_refresh_at": "2026-01-01T00:00:00.000Z",
"ordering_key": 1,
"paused_reason": "consecutive_failures",
"progress": 1,
"refresh_period": "15 min",
"refresh_status": "active",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
],
"legend_visibility": "hide",
"links": {
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6D"
},
"name": "text",
"ordering_key": 1,
"type": "layer_group",
"visibility_interaction": "default"
}
],
"layers": [
{
"attributes": [
{
"name": "text",
"type": "INTEGER"
}
],
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"last_refreshed_at": "2026-01-01T00:00:00.000Z",
"legend_display": "default",
"legend_visibility": "hide",
"links": {
"components": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/components",
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA"
},
"metadata": {
"attribution_text": "text",
"attribution_url": "text",
"description": "text",
"license": "text",
"source_abbreviation": "text",
"source_name": "text",
"source_url": "text",
"updated_at": "2025-03-24"
},
"name": "text",
"next_refresh_at": "2026-01-01T00:00:00.000Z",
"ordering_key": 1,
"paused_reason": "consecutive_failures",
"progress": 1,
"refresh_period": "15 min",
"refresh_status": "active",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
],
"links": {
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC"
},
"project_id": "text",
"public_access": "private",
"table_settings": {
"default_table_layer_id": "luCHyMruTQ6ozGk3gPJfEB",
"viewers_can_open_table": true
},
"thumbnail_url": "text",
"title": "text",
"type": "map",
"url": "text",
"viewer_permissions": {
"can_duplicate_map": true,
"can_export_data": true,
"can_see_map_presence": true
},
"visited_at": "text"
}Update map properties including title, description, and access permissions.
The ID of the map to update
The basemap to use for the map. Defaults to "default". Valid values are "default", "light", "dark", "satellite", a valid raster tile URL with {x}, {y}, and {z} parameters, or a hex color string like #ff0000.
A description to display in the map legend
The level of access to grant to the map. Defaults to "view_only".
luCHyMruTQ6ozGk3gPJfEBWhether viewers can open the data table
The new title for the map
Whether viewers can duplicate the map and data
Whether viewers can export map data
Whether viewers can see who else is viewing the map
Map
2024-05-25T15:51:34luCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBThe name of the attribute
The type of the attribute
luCHyMruTQ6ozGk3gPJfEBISO 8601 timestamp of when the layer's data was last updated. This includes scheduled refreshes, manual refreshes, and direct feature edits.
Controls how the layer is displayed in the legend.
Controls whether or not the layer is displayed in the legend. Defaults to "show".
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/componentshttps://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA2025-03-24ISO 8601 timestamp of when the next scheduled refresh will occur. Null if refresh is disabled or paused.
A sort order key used for ordering layers and layer groups in the legend
Why the layer's refresh is paused. Null when not paused.
Whether scheduled refresh is active, paused (due to failures), or disabled
The Felt Style Language style for the layer
Deprecated: use caption instead.
The tile URL for this layer
Controls how the layer group is displayed in the legend. Defaults to "show".
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6DA sort order key used for ordering layers and layer groups in the legend
Deprecated: use caption instead.
Controls how the layer group is displayed in the legend. Defaults to "default".
The name of the attribute
The type of the attribute
luCHyMruTQ6ozGk3gPJfEBISO 8601 timestamp of when the layer's data was last updated. This includes scheduled refreshes, manual refreshes, and direct feature edits.
Controls how the layer is displayed in the legend.
Controls whether or not the layer is displayed in the legend. Defaults to "show".
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/componentshttps://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA2025-03-24ISO 8601 timestamp of when the next scheduled refresh will occur. Null if refresh is disabled or paused.
A sort order key used for ordering layers and layer groups in the legend
Why the layer's refresh is paused. Null when not paused.
Whether scheduled refresh is active, paused (due to failures), or disabled
The Felt Style Language style for the layer
Deprecated: use caption instead.
The tile URL for this layer
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqCluCHyMruTQ6ozGk3gPJfEBWhether viewers can open the data table
A static thumbnail image of the map
Whether viewers can duplicate the map and data
Whether viewers can export map data
Whether viewers can see who else is viewing the map
UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
POST /api/v2/maps/{map_id}/update HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 278
{
"basemap": "text",
"description": "text",
"public_access": "private",
"table_settings": {
"default_table_layer_id": "luCHyMruTQ6ozGk3gPJfEB",
"viewers_can_open_table": true
},
"title": "text",
"viewer_permissions": {
"can_duplicate_map": true,
"can_export_data": true,
"can_see_map_presence": true
}
}{
"basemap": "text",
"created_at": "2024-05-25T15:51:34",
"element_groups": [
{
"elements": {
"features": [
{
"geometry": {
"felt:id": "luCHyMruTQ6ozGk3gPJfEB",
"felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
},
"properties": {},
"type": "Feature"
}
],
"type": "FeatureCollection"
},
"id": "luCHyMruTQ6ozGk3gPJfEB",
"name": "text"
}
],
"elements": {
"features": [
{
"geometry": {
"felt:id": "luCHyMruTQ6ozGk3gPJfEB",
"felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
},
"properties": {},
"type": "Feature"
}
],
"type": "FeatureCollection"
},
"folder_id": "text",
"id": "luCHyMruTQ6ozGk3gPJfEB",
"layer_groups": [
{
"caption": "text",
"id": "luCHyMruTQ6ozGk3gPJfEB",
"layers": [
{
"attributes": [
{
"name": "text",
"type": "INTEGER"
}
],
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"last_refreshed_at": "2026-01-01T00:00:00.000Z",
"legend_display": "default",
"legend_visibility": "hide",
"links": {
"components": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/components",
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA"
},
"metadata": {
"attribution_text": "text",
"attribution_url": "text",
"description": "text",
"license": "text",
"source_abbreviation": "text",
"source_name": "text",
"source_url": "text",
"updated_at": "2025-03-24"
},
"name": "text",
"next_refresh_at": "2026-01-01T00:00:00.000Z",
"ordering_key": 1,
"paused_reason": "consecutive_failures",
"progress": 1,
"refresh_period": "15 min",
"refresh_status": "active",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
],
"legend_visibility": "hide",
"links": {
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6D"
},
"name": "text",
"ordering_key": 1,
"type": "layer_group",
"visibility_interaction": "default"
}
],
"layers": [
{
"attributes": [
{
"name": "text",
"type": "INTEGER"
}
],
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"last_refreshed_at": "2026-01-01T00:00:00.000Z",
"legend_display": "default",
"legend_visibility": "hide",
"links": {
"components": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/components",
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA"
},
"metadata": {
"attribution_text": "text",
"attribution_url": "text",
"description": "text",
"license": "text",
"source_abbreviation": "text",
"source_name": "text",
"source_url": "text",
"updated_at": "2025-03-24"
},
"name": "text",
"next_refresh_at": "2026-01-01T00:00:00.000Z",
"ordering_key": 1,
"paused_reason": "consecutive_failures",
"progress": 1,
"refresh_period": "15 min",
"refresh_status": "active",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
],
"links": {
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC"
},
"project_id": "text",
"public_access": "private",
"table_settings": {
"default_table_layer_id": "luCHyMruTQ6ozGk3gPJfEB",
"viewers_can_open_table": true
},
"thumbnail_url": "text",
"title": "text",
"type": "map",
"url": "text",
"viewer_permissions": {
"can_duplicate_map": true,
"can_export_data": true,
"can_see_map_presence": true
},
"visited_at": "text"
}Retrieve a map with its metadata including title, URL, thumbnail, and timestamps.
Map
2024-05-25T15:51:34luCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBThe name of the attribute
The type of the attribute
luCHyMruTQ6ozGk3gPJfEBISO 8601 timestamp of when the layer's data was last updated. This includes scheduled refreshes, manual refreshes, and direct feature edits.
Controls how the layer is displayed in the legend.
Controls whether or not the layer is displayed in the legend. Defaults to "show".
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/componentshttps://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA2025-03-24ISO 8601 timestamp of when the next scheduled refresh will occur. Null if refresh is disabled or paused.
A sort order key used for ordering layers and layer groups in the legend
Why the layer's refresh is paused. Null when not paused.
Whether scheduled refresh is active, paused (due to failures), or disabled
The Felt Style Language style for the layer
Deprecated: use caption instead.
The tile URL for this layer
Controls how the layer group is displayed in the legend. Defaults to "show".
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6DA sort order key used for ordering layers and layer groups in the legend
Deprecated: use caption instead.
Controls how the layer group is displayed in the legend. Defaults to "default".
The name of the attribute
The type of the attribute
luCHyMruTQ6ozGk3gPJfEBISO 8601 timestamp of when the layer's data was last updated. This includes scheduled refreshes, manual refreshes, and direct feature edits.
Controls how the layer is displayed in the legend.
Controls whether or not the layer is displayed in the legend. Defaults to "show".
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/componentshttps://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA2025-03-24ISO 8601 timestamp of when the next scheduled refresh will occur. Null if refresh is disabled or paused.
A sort order key used for ordering layers and layer groups in the legend
Why the layer's refresh is paused. Null when not paused.
Whether scheduled refresh is active, paused (due to failures), or disabled
The Felt Style Language style for the layer
Deprecated: use caption instead.
The tile URL for this layer
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqCluCHyMruTQ6ozGk3gPJfEBWhether viewers can open the data table
A static thumbnail image of the map
Whether viewers can duplicate the map and data
Whether viewers can export map data
Whether viewers can see who else is viewing the map
UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
GET /api/v2/maps/{map_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
"basemap": "text",
"created_at": "2024-05-25T15:51:34",
"element_groups": [
{
"elements": {
"features": [
{
"geometry": {
"felt:id": "luCHyMruTQ6ozGk3gPJfEB",
"felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
},
"properties": {},
"type": "Feature"
}
],
"type": "FeatureCollection"
},
"id": "luCHyMruTQ6ozGk3gPJfEB",
"name": "text"
}
],
"elements": {
"features": [
{
"geometry": {
"felt:id": "luCHyMruTQ6ozGk3gPJfEB",
"felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
},
"properties": {},
"type": "Feature"
}
],
"type": "FeatureCollection"
},
"folder_id": "text",
"id": "luCHyMruTQ6ozGk3gPJfEB",
"layer_groups": [
{
"caption": "text",
"id": "luCHyMruTQ6ozGk3gPJfEB",
"layers": [
{
"attributes": [
{
"name": "text",
"type": "INTEGER"
}
],
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"last_refreshed_at": "2026-01-01T00:00:00.000Z",
"legend_display": "default",
"legend_visibility": "hide",
"links": {
"components": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/components",
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA"
},
"metadata": {
"attribution_text": "text",
"attribution_url": "text",
"description": "text",
"license": "text",
"source_abbreviation": "text",
"source_name": "text",
"source_url": "text",
"updated_at": "2025-03-24"
},
"name": "text",
"next_refresh_at": "2026-01-01T00:00:00.000Z",
"ordering_key": 1,
"paused_reason": "consecutive_failures",
"progress": 1,
"refresh_period": "15 min",
"refresh_status": "active",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
],
"legend_visibility": "hide",
"links": {
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6D"
},
"name": "text",
"ordering_key": 1,
"type": "layer_group",
"visibility_interaction": "default"
}
],
"layers": [
{
"attributes": [
{
"name": "text",
"type": "INTEGER"
}
],
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"last_refreshed_at": "2026-01-01T00:00:00.000Z",
"legend_display": "default",
"legend_visibility": "hide",
"links": {
"components": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/components",
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA"
},
"metadata": {
"attribution_text": "text",
"attribution_url": "text",
"description": "text",
"license": "text",
"source_abbreviation": "text",
"source_name": "text",
"source_url": "text",
"updated_at": "2025-03-24"
},
"name": "text",
"next_refresh_at": "2026-01-01T00:00:00.000Z",
"ordering_key": 1,
"paused_reason": "consecutive_failures",
"progress": 1,
"refresh_period": "15 min",
"refresh_status": "active",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
],
"links": {
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC"
},
"project_id": "text",
"public_access": "private",
"table_settings": {
"default_table_layer_id": "luCHyMruTQ6ozGk3gPJfEB",
"viewers_can_open_table": true
},
"thumbnail_url": "text",
"title": "text",
"type": "map",
"url": "text",
"viewer_permissions": {
"can_duplicate_map": true,
"can_export_data": true,
"can_see_map_presence": true
},
"visited_at": "text"
}Permanently delete a map and all its associated data.
The ID of the map to delete
No Content
No content
UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
DELETE /api/v2/maps/{map_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
No content
Generate a direct download link for layer data export.
Get a link to export a layer as a GeoPackage (vector layers) or GeoTIFF (raster layers).
The ID of the map where the layer is located
The ID of the layer to export
Export link
UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
ServiceUnavailableError
GET /api/v2/maps/{map_id}/layers/{layer_id}/get_export_link HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
"export_link": "text"
}Check the processing status and download availability of a custom export request.
If the export is successful, the response will include a download_url for accessing the exported data.
The ID of the map where the layer is located
The ID of the layer to export
The ID of the export
Custom export request status
https://us1.data-pipeline.felt.com/fcdfd96c-06fa-40b9-9ae9-ad034b5a66df/Felt-Export.zipFZWQjWZJSZWvW3yn9BeV9AyAcompletedPossible values: UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
GET /api/v2/maps/{map_id}/layers/{layer_id}/custom_exports/{export_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
"download_url": "https://us1.data-pipeline.felt.com/fcdfd96c-06fa-40b9-9ae9-ad034b5a66df/Felt-Export.zip",
"export_id": "FZWQjWZJSZWvW3yn9BeV9AyA",
"filters": [],
"status": "completed"
}Start a custom export with specific format and filter options for layer data.
Export requests are asynchronous. A successful response will return a poll_endpoint to check the status of the export using the poll custom export endpoint.
The ID of the map where the layer is located
The ID of the layer to export
Send an email to the requesting user when the export completes. Defaults to true
csvPossible values: Custom export response
luCHyMruTQ6ozGk3gPJfEBhttp://felt.com/api/v2/maps/vAbZ5eKqRoGe4sCH8nHW8D/layers/7kF9Cfz45TUWIiuuWV8uZ7A/custom_exports/auFxn9BO4RrGGiKrGfaS7ZBUnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
ServiceUnavailableError
POST /api/v2/maps/{map_id}/layers/{layer_id}/custom_export HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 63
{
"email_on_completion": true,
"filters": [],
"output_format": "csv"
}{
"export_request_id": "luCHyMruTQ6ozGk3gPJfEB",
"poll_endpoint": "http://felt.com/api/v2/maps/vAbZ5eKqRoGe4sCH8nHW8D/layers/7kF9Cfz45TUWIiuuWV8uZ7A/custom_exports/auFxn9BO4RrGGiKrGfaS7ZB"
}Update data source connection settings, access permissions, or configuration details.
Connecting the Source and inspecting its datasets will happen asynchronously after the API response is returned. To determine when the inspection process has completed, poll the Show Source endpoint and check for sync_status: completed.
The ID of the source to update
ABS blob storage URL
BigQuery credentials - Base 64 encoded Service account JSON
BigQuery dataset
BigQuery project
Databricks catalog
Databricks server HTTP path
Databricks schema
Databricks server hostname
ESRI server token
ESRI FeatureServer, MapServer, or ImageServer URL
GCS URI
MSSQL database name
MSSQL host
MSSQL password
MSSQL port
MSSQL user name
Postgres database name
Postgres host
Postgres password
Postgres port
Postgres schema
Postgres user name
Redshift database name
Redshift host
Redshift password
Redshift port
Redshift user name
S3 URI
Snowflake account ID
Snowflake database name
Snowflake password
Snowflake role
Snowflake database schema
Snowflake user name
Snowflake warehouse
STAC token
STAC server / asset URL
WFS URL
WMS/WMTS URL
luCHyMruTQ6ozGk3gPJfEBSource reference
luCHyMruTQ6ozGk3gPJfEBhttps://felt.com/api/v2/sources/V0dnOMOuTd9B9BOsL9C0UjmqCluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBUnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
POST /api/v2/sources/{source_id}/update HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 119
{
"connection": {
"blob_storage_url": "text",
"type": "abs_bucket"
},
"name": "text",
"permissions": {
"type": "workspace_editors"
}
}{
"automatic_sync": "enabled",
"connection_type": "abs_bucket",
"created_at": 1,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"last_synced_at": 1,
"links": {
"self": "https://felt.com/api/v2/sources/V0dnOMOuTd9B9BOsL9C0UjmqC"
},
"name": "text",
"owner_id": "luCHyMruTQ6ozGk3gPJfEB",
"permissions": {
"type": "workspace_editors"
},
"sync_status": "syncing",
"type": "source_reference",
"updated_at": 1,
"workspace_id": "luCHyMruTQ6ozGk3gPJfEB"
}Retrieve all data sources accessible to the authenticated user within the workspace.
Only needed when using the API as part of a plugin
Source references
luCHyMruTQ6ozGk3gPJfEBhttps://felt.com/api/v2/sources/V0dnOMOuTd9B9BOsL9C0UjmqCluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBUnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
GET /api/v2/sources HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
[
{
"automatic_sync": "enabled",
"connection_type": "abs_bucket",
"created_at": 1,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"last_synced_at": 1,
"links": {
"self": "https://felt.com/api/v2/sources/V0dnOMOuTd9B9BOsL9C0UjmqC"
},
"name": "text",
"owner_id": "luCHyMruTQ6ozGk3gPJfEB",
"permissions": {
"type": "workspace_editors"
},
"sync_status": "syncing",
"type": "source_reference",
"updated_at": 1,
"workspace_id": "luCHyMruTQ6ozGk3gPJfEB"
}
]Create a new data source connection with authentication credentials and access permissions.
Connecting the Source and inspecting its datasets will happen asynchronously after the API response is returned. To determine when the inspection process has completed, poll the Show Source endpoint and check for sync_status: completed.
ABS blob storage URL
BigQuery credentials - Base 64 encoded Service account JSON
BigQuery dataset
BigQuery project
Databricks catalog
Databricks server HTTP path
Databricks schema
Databricks server hostname
ESRI server token
ESRI FeatureServer, MapServer, or ImageServer URL
GCS URI
MSSQL database name
MSSQL host
MSSQL password
MSSQL port
MSSQL user name
Postgres database name
Postgres host
Postgres password
Postgres port
Postgres schema
Postgres user name
Redshift database name
Redshift host
Redshift password
Redshift port
Redshift user name
S3 URI
Snowflake account ID
Snowflake database name
Snowflake password
Snowflake role
Snowflake database schema
Snowflake user name
Snowflake warehouse
The header name
Whether or not the header is sensitive. If it is marked as sensitive, then felt:redacted will be returned when viewing this header
The header value
STAC token
STAC server / asset URL
WFS URL
WMS/WMTS URL
luCHyMruTQ6ozGk3gPJfEBSource reference
luCHyMruTQ6ozGk3gPJfEBhttps://felt.com/api/v2/sources/V0dnOMOuTd9B9BOsL9C0UjmqCluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBUnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
POST /api/v2/sources HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 269
{
"connection": {
"blob_storage_url": "text",
"credentials": [
{
"credential": {
"connection_string": "text",
"type": "azure_storage_connection_string"
},
"name": "text",
"use_case": "source_authentication"
}
],
"type": "abs_bucket"
},
"name": "text",
"permissions": {
"type": "workspace_editors"
}
}{
"automatic_sync": "enabled",
"connection_type": "abs_bucket",
"created_at": 1,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"last_synced_at": 1,
"links": {
"self": "https://felt.com/api/v2/sources/V0dnOMOuTd9B9BOsL9C0UjmqC"
},
"name": "text",
"owner_id": "luCHyMruTQ6ozGk3gPJfEB",
"permissions": {
"type": "workspace_editors"
},
"sync_status": "syncing",
"type": "source_reference",
"updated_at": 1,
"workspace_id": "luCHyMruTQ6ozGk3gPJfEB"
}Trigger a full data synchronization from the source to update all connected layers with latest data.
Syncing will happen asynchronously after the API response is returned. To determine when the inspection process has completed, poll the Show Source endpoint and check for sync_status: completed.
The ID of the source to sync
Source reference
luCHyMruTQ6ozGk3gPJfEBhttps://felt.com/api/v2/sources/V0dnOMOuTd9B9BOsL9C0UjmqCluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBUnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
POST /api/v2/sources/{source_id}/sync HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
"automatic_sync": "enabled",
"connection_type": "abs_bucket",
"created_at": 1,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"last_synced_at": 1,
"links": {
"self": "https://felt.com/api/v2/sources/V0dnOMOuTd9B9BOsL9C0UjmqC"
},
"name": "text",
"owner_id": "luCHyMruTQ6ozGk3gPJfEB",
"permissions": {
"type": "workspace_editors"
},
"sync_status": "syncing",
"type": "source_reference",
"updated_at": 1,
"workspace_id": "luCHyMruTQ6ozGk3gPJfEB"
}Retrieve detailed configuration and connection information for a specific data source.
The ID of the source to show
Source
luCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBBigQuery dataset to index. If omitted all datasets will be indexed
BigQuery project to index
luCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBThe header name
Whether or not the header is sensitive. If it is marked as sensitive, then felt:redacted will be returned when viewing this header
The header value
luCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBUnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
GET /api/v2/sources/{source_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
"automatic_sync": "enabled",
"connection": {
"blob_storage_url": "text",
"credentials": [
{
"created_at": 1,
"credential": {
"connection_string": "text",
"type": "azure_storage_connection_string"
},
"id": "luCHyMruTQ6ozGk3gPJfEB",
"name": "text",
"source_id": "luCHyMruTQ6ozGk3gPJfEB",
"updated_at": 1,
"use_case": "source_authentication"
}
],
"type": "abs_bucket"
},
"created_at": 1,
"datasets": [
{
"created_at": 1,
"description": "text",
"geometry_type": "polygon",
"id": "luCHyMruTQ6ozGk3gPJfEB",
"inspection_status": "completed",
"name": "text",
"type": "dataset",
"updated_at": 1
}
],
"id": "luCHyMruTQ6ozGk3gPJfEB",
"last_synced_at": 1,
"name": "text",
"owner_id": "luCHyMruTQ6ozGk3gPJfEB",
"permissions": {
"type": "workspace_editors"
},
"sync_status": "syncing",
"type": "source",
"updated_at": 1,
"workspace_id": "luCHyMruTQ6ozGk3gPJfEB"
}Permanently delete a data source connection and all its associated layers and data.
The ID of the source to delete
No Content
No content
UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
DELETE /api/v2/sources/{source_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
No content
Add authentication credentials to an existing data source for secure access.
Some sources may need to be configured with additional credentials to work with Felt. Access to S3 Buckets, for example, may be protected by IAM policies. Adding a SourceCredential-AwsAssumeRole credential to your S3 Bucket source allows Felt to connect to a private source.
Sensitive fields in credentials, like SourceCredential-KeyPair.private_key, will be returned as felt:redacted.
The ID of the source to attach the credential
The header name
Whether or not the header is sensitive. If it is marked as sensitive, then felt:redacted will be returned when viewing this header
The header value
Source credential created
The header name
Whether or not the header is sensitive. If it is marked as sensitive, then felt:redacted will be returned when viewing this header
The header value
luCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBUnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
POST /api/v2/sources/{source_id}/credentials HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 137
{
"credential": {
"role_arn": "text",
"role_session_name": "text",
"type": "aws_assume_role"
},
"name": "text",
"use_case": "stac_api_authentication"
}{
"created_at": 1,
"credential": {
"role_arn": "text",
"role_session_name": "text",
"type": "aws_assume_role"
},
"id": "luCHyMruTQ6ozGk3gPJfEB",
"name": "text",
"source_id": "luCHyMruTQ6ozGk3gPJfEB",
"updated_at": 1,
"use_case": "stac_api_authentication"
}Update existing authentication credentials for a data source connection.
Sensitive fields in credentials, like SourceCredential-KeyPair.private_key, will be returned as felt:redacted.
The ID of the source that the credential belongs to
The ID of the credential
The header name
Whether or not the header is sensitive. If it is marked as sensitive, then felt:redacted will be returned when viewing this header
The header value
Source credential updated
The header name
Whether or not the header is sensitive. If it is marked as sensitive, then felt:redacted will be returned when viewing this header
The header value
luCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBUnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
POST /api/v2/sources/{source_id}/credentials/{credential_id}/update HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 137
{
"credential": {
"role_arn": "text",
"role_session_name": "text",
"type": "aws_assume_role"
},
"name": "text",
"use_case": "stac_api_authentication"
}{
"created_at": 1,
"credential": {
"role_arn": "text",
"role_session_name": "text",
"type": "aws_assume_role"
},
"id": "luCHyMruTQ6ozGk3gPJfEB",
"name": "text",
"source_id": "luCHyMruTQ6ozGk3gPJfEB",
"updated_at": 1,
"use_case": "stac_api_authentication"
}Remove authentication credentials from a data source connection.
The ID of the source that the credential belongs to
The ID of the credential to delete
No Content
No content
UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
DELETE /api/v2/sources/{source_id}/credentials/{credential_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
No content
Creates a token, valid for 12 hours, for authenticating a visitor to view a private embedded map view without being logged into Felt. You must provide a user_email to associate the token with the end user that will be viewing the map. Each end user should be a member of your Felt workspace with a viewer, editor, or admin role assigned.
Mint a fresh token for each page load. The 12 hours cover a full working session in one open tab — reconnects included — and when they run out the embed asks the visitor to refresh, at which point your page should mint again.
Generate a token by making a call to this API from your server
Securely pass the token to your frontend client
Each token must be associated with the email address of the user who will use it.
EmbedToken
2024-05-25T15:51:34UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
POST /api/v2/maps/{map_id}/embed_token?user_email=text HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
"expires_at": "2024-05-25T15:51:34",
"token": "text"
}Render the map to a PNG, JPG or PDF image at a chosen size and view — what the app's "Export as image" produces, without a browser.
The request waits up to 55 seconds and returns 200 with a completed export. Follow download_url to download the file before expires_at. Invalid requests return 4xx; rendering failures return 503, and a render that exceeds the request deadline returns 504. There is no polling endpoint.
The ID of the map to export
Output settings, geographic view and legend options for a map image export.
Include the map's layer components (charts, statistics) in the legend card. Defaults to true.
trueInclude the map's description under the title. Defaults to true.
trueHead the legend card with the map's title. Defaults to true.
trueDraw the legend card over the map. Ignored when the map has no legend. Defaults to true.
trueOutput format. JPG is much smaller for satellite imagery. Defaults to png.
pngExample: pngPossible values: Height of the output image file, in pixels.
1500JPG quality, 0–1. Only used when format is jpg. Defaults to 0.9.
0.9Example: 0.9Output pixels per CSS pixel of the map canvas (the app's Resolution setting). The canvas is width_px / pixel_ratio × height_px / pixel_ratio, so raising this at a fixed output size covers less ground. Defaults to 2.
2Example: 2Possible values: Width of the output image file, in pixels.
2000Draw the scale bar. Attribution is always drawn. Defaults to true.
trueChoose exactly one of bounds or viewport. Omit view to use the map's default view.
[west, south, east, north] in degrees. The image is zoomed so the whole box is visible; the box is centered and the shorter axis is padded. The response's rendered_bounds reports the resulting frame. Latitudes must be within -85.051129..85.051129. A box that cannot fit at the supported zoom levels is rejected.
[-122.52,37.7,-122.35,37.83]37.77-122.4312.5Completed image export
A URL for the finished image; it redirects to the file, so follow redirects. Expires at expires_at.
https://felt.com/map-image-exports/auFxn9BO4RrGGiKrGfaS7ZB/downloadWhen download_url stops working.
2026-08-28T19:04:11ZMy map.pngluCHyMruTQ6ozGk3gPJfEBOutput format. JPG is much smaller for satellite imagery. Defaults to png.
pngExample: pngPossible values: Height of the output image file, in pixels.
1500JPG quality, 0–1. Only used when format is jpg. Defaults to 0.9.
0.9Example: 0.9Output pixels per CSS pixel of the map canvas (the app's Resolution setting). The canvas is width_px / pixel_ratio × height_px / pixel_ratio, so raising this at a fixed output size covers less ground. Defaults to 2.
2Example: 2Possible values: Width of the output image file, in pixels.
2000The [west, south, east, north] the image actually covers — the requested view.bounds fitted to the canvas's aspect ratio, or the viewport's frame. Adjust framing from this rather than by measuring pixels. An east west of west crosses the antimeridian.
[-129,41.25,-116.1,47.03]UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
ServiceUnavailableError
Gateway timeout
POST /api/v2/maps/{map_id}/image_exports HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 254
{
"legend": {
"show_components": true,
"show_description": true,
"show_title": true,
"visible": true
},
"output": {
"format": "png",
"height_px": 1500,
"jpg_quality": 0.9,
"pixel_ratio": 2,
"width_px": 2000
},
"show_scale_bar": true,
"view": {
"bounds": [
-122.52,
37.7,
-122.35,
37.83
]
}
}{
"download_url": "https://felt.com/map-image-exports/auFxn9BO4RrGGiKrGfaS7ZB/download",
"expires_at": "2026-08-28T19:04:11Z",
"filename": "My map.png",
"id": "luCHyMruTQ6ozGk3gPJfEB",
"output": {
"format": "png",
"height_px": 1500,
"jpg_quality": 0.9,
"pixel_ratio": 2,
"width_px": 2000
},
"rendered_bounds": [
-129,
41.25,
-116.1,
47.03
]
}Permanently delete an element from a map.
The ID of the map to delete the element from.
The ID of the element to delete.
No Content
No content
UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
DELETE /api/v2/maps/{map_id}/elements/{element_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
No content
Returns a GeoJSON FeatureCollection containing all the elements in a map that are not in an element group.
The ID of the map to list elements from.
GeoJSON
luCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBUnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
GET /api/v2/maps/{map_id}/elements HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
"features": [
{
"geometry": {
"felt:id": "luCHyMruTQ6ozGk3gPJfEB",
"felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
},
"properties": {},
"type": "Feature"
}
],
"type": "FeatureCollection"
}Create new elements or update existing ones on a map using GeoJSON data. Each element is represented by a feature in the POST'ed GeoJSON Feature Collection. For each feature, including an existing element id will result in the element being updated on the map. If no element id is provided (or a non-existent one), a new element will be created.
The ID of the map to create the elements in
luCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBGeoJSON
luCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBUnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
POST /api/v2/maps/{map_id}/elements HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 165
{
"features": [
{
"geometry": {
"felt:id": "luCHyMruTQ6ozGk3gPJfEB",
"felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
},
"properties": {},
"type": "Feature"
}
],
"type": "FeatureCollection"
}{
"features": [
{
"geometry": {
"felt:id": "luCHyMruTQ6ozGk3gPJfEB",
"felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
},
"properties": {},
"type": "Feature"
}
],
"type": "FeatureCollection"
}Retrieve all elements from a specific group as GeoJSON.
The ID of the map.
The ID of the element group.
GeoJSON
luCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBUnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
GET /api/v2/maps/{map_id}/element_groups/{group_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
"features": [
{
"geometry": {
"felt:id": "luCHyMruTQ6ozGk3gPJfEB",
"felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
},
"properties": {},
"type": "Feature"
}
],
"type": "FeatureCollection"
}Returns a list of GeoJSON FeatureCollections, one for each element group in the map.
The ID of the map to list groups from.
ElementGroupList
The color of the element group symbol.
luCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBThe ID of the element group.
The name of the element group.
The symbol used to represent the element group.
UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
GET /api/v2/maps/{map_id}/element_groups HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
[
{
"color": "text",
"elements": {
"features": [
{
"geometry": {
"felt:id": "luCHyMruTQ6ozGk3gPJfEB",
"felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
},
"properties": {},
"type": "Feature"
}
],
"type": "FeatureCollection"
},
"id": "text",
"name": "text",
"symbol": "text"
}
]Create new element groups or update existing ones.
For each Element Group, including an existing Element Group id will result in the Element Group being updated. If no id is provided, a new Element Group will be created.
The ID of the map to create the group in
#C93535luCHyMruTQ6ozGk3gPJfEBMy Element GroupdotElement group list
The color of the element group symbol.
luCHyMruTQ6ozGk3gPJfEBluCHyMruTQ6ozGk3gPJfEBThe ID of the element group.
The name of the element group.
The symbol used to represent the element group.
UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
POST /api/v2/maps/{map_id}/element_groups HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 92
[
{
"color": "#C93535",
"id": "luCHyMruTQ6ozGk3gPJfEB",
"name": "My Element Group",
"symbol": "dot"
}
][
{
"color": "text",
"elements": {
"features": [
{
"geometry": {
"felt:id": "luCHyMruTQ6ozGk3gPJfEB",
"felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
},
"properties": {},
"type": "Feature"
}
],
"type": "FeatureCollection"
},
"id": "text",
"name": "text",
"symbol": "text"
}
]Export all comments and replies from a map in CSV, JSON, or GeoJSON format.
The ID of the map to export comments from.
The format to export the comments in: 'csv', 'json' (default), or 'geojson'
Comment export response
Comment Thread
Comment Thread
UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
GET /api/v2/maps/{map_id}/comments/export HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
[]Mark a comment thread as resolved.
The ID of the map that contains the comment.
The ID of the comment to resolve.
Comment resolved response
luCHyMruTQ6ozGk3gPJfEBUnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
POST /api/v2/maps/{map_id}/comments/{comment_id}/resolve HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
"comment_id": "luCHyMruTQ6ozGk3gPJfEB"
}Permanently delete a comment or reply from the map.
The ID of the map that contains the comment.
The ID of the comment to delete.
No Content
No content
UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
DELETE /api/v2/maps/{map_id}/comments/{comment_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
No content
Make a layer available in the workspace library for reuse by team members.
The ID of the map where the layer is located
The ID of the layer to publish
The Felt Server to publish into. Defaults to the workspace's default Felt Server.
8f9a2b1c-3d4e-5f60-7a8b-9c0d1e2f3a4bThe name to publish the layer under
My LayerPublish layer response
The name of the attribute
The type of the attribute
luCHyMruTQ6ozGk3gPJfEBISO 8601 timestamp of when the layer's data was last updated. This includes scheduled refreshes, manual refreshes, and direct feature edits.
Controls how the layer is displayed in the legend.
Controls whether or not the layer is displayed in the legend. Defaults to "show".
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/componentshttps://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA2025-03-24ISO 8601 timestamp of when the next scheduled refresh will occur. Null if refresh is disabled or paused.
A sort order key used for ordering layers and layer groups in the legend
Why the layer's refresh is paused. Null when not paused.
Whether scheduled refresh is active, paused (due to failures), or disabled
The Felt Style Language style for the layer
Deprecated: use caption instead.
The tile URL for this layer
UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
POST /api/v2/maps/{map_id}/layers/{layer_id}/publish HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 73
{
"feltServerId": "8f9a2b1c-3d4e-5f60-7a8b-9c0d1e2f3a4b",
"name": "My Layer"
}{
"attributes": [
{
"name": "text",
"type": "INTEGER"
}
],
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"last_refreshed_at": "2026-01-01T00:00:00.000Z",
"legend_display": "default",
"legend_visibility": "hide",
"links": {
"components": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/components",
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA"
},
"metadata": {
"attribution_text": "text",
"attribution_url": "text",
"description": "text",
"license": "text",
"source_abbreviation": "text",
"source_name": "text",
"source_url": "text",
"updated_at": "2025-03-24"
},
"name": "text",
"next_refresh_at": "2026-01-01T00:00:00.000Z",
"ordering_key": 1,
"paused_reason": "consecutive_failures",
"progress": 1,
"refresh_period": "15 min",
"refresh_status": "active",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}Make a layer group available in the workspace library for reuse by team members.
The ID of the map where the layer group is located
The ID of the layer group to publish
The Felt Server to publish into. Defaults to the workspace's default Felt Server.
8f9a2b1c-3d4e-5f60-7a8b-9c0d1e2f3a4bThe name to publish the layer group under
My LayerPublish layer group response
luCHyMruTQ6ozGk3gPJfEBThe name of the attribute
The type of the attribute
luCHyMruTQ6ozGk3gPJfEBISO 8601 timestamp of when the layer's data was last updated. This includes scheduled refreshes, manual refreshes, and direct feature edits.
Controls how the layer is displayed in the legend.
Controls whether or not the layer is displayed in the legend. Defaults to "show".
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/componentshttps://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA2025-03-24ISO 8601 timestamp of when the next scheduled refresh will occur. Null if refresh is disabled or paused.
A sort order key used for ordering layers and layer groups in the legend
Why the layer's refresh is paused. Null when not paused.
Whether scheduled refresh is active, paused (due to failures), or disabled
The Felt Style Language style for the layer
Deprecated: use caption instead.
The tile URL for this layer
Controls how the layer group is displayed in the legend. Defaults to "show".
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6DA sort order key used for ordering layers and layer groups in the legend
Deprecated: use caption instead.
Controls how the layer group is displayed in the legend. Defaults to "default".
UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
POST /api/v2/maps/{map_id}/layer_groups/{layer_group_id}/publish HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 73
{
"feltServerId": "8f9a2b1c-3d4e-5f60-7a8b-9c0d1e2f3a4b",
"name": "My Layer"
}{
"caption": "text",
"id": "luCHyMruTQ6ozGk3gPJfEB",
"layers": [
{
"attributes": [
{
"name": "text",
"type": "INTEGER"
}
],
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"last_refreshed_at": "2026-01-01T00:00:00.000Z",
"legend_display": "default",
"legend_visibility": "hide",
"links": {
"components": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/components",
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA"
},
"metadata": {
"attribution_text": "text",
"attribution_url": "text",
"description": "text",
"license": "text",
"source_abbreviation": "text",
"source_name": "text",
"source_url": "text",
"updated_at": "2025-03-24"
},
"name": "text",
"next_refresh_at": "2026-01-01T00:00:00.000Z",
"ordering_key": 1,
"paused_reason": "consecutive_failures",
"progress": 1,
"refresh_period": "15 min",
"refresh_status": "active",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
],
"legend_visibility": "hide",
"links": {
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6D"
},
"name": "text",
"ordering_key": 1,
"type": "layer_group",
"visibility_interaction": "default"
}List all layers in your workspace's library, or the felt layer library.
You can add a layer from the library to a map by using the "Duplicate layers" API endpoint (POST /api/v2/duplicate_layers) and the layer id provided by this endpoint.
Defaults to listing library layers for your "workspace". Use "felt" to list layers from the Felt data library. Use "all" to list layers from both sources.
workspacePossible values: LayerLibrary
luCHyMruTQ6ozGk3gPJfEBThe name of the attribute
The type of the attribute
luCHyMruTQ6ozGk3gPJfEBISO 8601 timestamp of when the layer's data was last updated. This includes scheduled refreshes, manual refreshes, and direct feature edits.
Controls how the layer is displayed in the legend.
Controls whether or not the layer is displayed in the legend. Defaults to "show".
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/componentshttps://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA2025-03-24ISO 8601 timestamp of when the next scheduled refresh will occur. Null if refresh is disabled or paused.
A sort order key used for ordering layers and layer groups in the legend
Why the layer's refresh is paused. Null when not paused.
Whether scheduled refresh is active, paused (due to failures), or disabled
The Felt Style Language style for the layer
Deprecated: use caption instead.
The tile URL for this layer
Controls how the layer group is displayed in the legend. Defaults to "show".
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6DA sort order key used for ordering layers and layer groups in the legend
Deprecated: use caption instead.
Controls how the layer group is displayed in the legend. Defaults to "default".
The name of the attribute
The type of the attribute
luCHyMruTQ6ozGk3gPJfEBISO 8601 timestamp of when the layer's data was last updated. This includes scheduled refreshes, manual refreshes, and direct feature edits.
Controls how the layer is displayed in the legend.
Controls whether or not the layer is displayed in the legend. Defaults to "show".
https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/componentshttps://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA2025-03-24ISO 8601 timestamp of when the next scheduled refresh will occur. Null if refresh is disabled or paused.
A sort order key used for ordering layers and layer groups in the legend
Why the layer's refresh is paused. Null when not paused.
Whether scheduled refresh is active, paused (due to failures), or disabled
The Felt Style Language style for the layer
Deprecated: use caption instead.
The tile URL for this layer
UnauthorizedError
UnauthorizedError
NotFoundError
Unprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueUnprocessable Entity
null value where string expected/data/attributes/petNameInvalid valueInternalServerError
GET /api/v2/library HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
"layer_groups": [
{
"caption": "text",
"id": "luCHyMruTQ6ozGk3gPJfEB",
"layers": [
{
"attributes": [
{
"name": "text",
"type": "INTEGER"
}
],
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"last_refreshed_at": "2026-01-01T00:00:00.000Z",
"legend_display": "default",
"legend_visibility": "hide",
"links": {
"components": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/components",
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA"
},
"metadata": {
"attribution_text": "text",
"attribution_url": "text",
"description": "text",
"license": "text",
"source_abbreviation": "text",
"source_name": "text",
"source_url": "text",
"updated_at": "2025-03-24"
},
"name": "text",
"next_refresh_at": "2026-01-01T00:00:00.000Z",
"ordering_key": 1,
"paused_reason": "consecutive_failures",
"progress": 1,
"refresh_period": "15 min",
"refresh_status": "active",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
],
"legend_visibility": "hide",
"links": {
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6D"
},
"name": "text",
"ordering_key": 1,
"type": "layer_group",
"visibility_interaction": "default"
}
],
"layers": [
{
"attributes": [
{
"name": "text",
"type": "INTEGER"
}
],
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"last_refreshed_at": "2026-01-01T00:00:00.000Z",
"legend_display": "default",
"legend_visibility": "hide",
"links": {
"components": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA/components",
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layers/k441enUxQUOnZqc1ZvNsDA"
},
"metadata": {
"attribution_text": "text",
"attribution_url": "text",
"description": "text",
"license": "text",
"source_abbreviation": "text",
"source_name": "text",
"source_url": "text",
"updated_at": "2025-03-24"
},
"name": "text",
"next_refresh_at": "2026-01-01T00:00:00.000Z",
"ordering_key": 1,
"paused_reason": "consecutive_failures",
"progress": 1,
"refresh_period": "15 min",
"refresh_status": "active",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
],
"type": "layer_library"
}Caution: Deleting a project deletes all of the folders and maps inside!
This cannot be undone.
This action cannot be undone. The layer group and all its contained layers will be permanently removed from the map.
This action cannot be undone. The layer and all its data will be permanently removed from the map.
This action cannot be undone. The map and all its layers, elements, and comments will be permanently removed.
Any layers created from the Source will remain after it is deleted, but they will no longer be refreshed.
This action cannot be undone. The element will be permanently removed from the map.
This action cannot be undone. The comment or reply will be permanently removed from the map.
Layer files aren't uploaded directly to the Felt API. Instead, they are uploaded to an S3 bucket.
The response to this API request will include a URL and pre-signed params for you to use to upload your file. Only a single file may be uploaded — if you wish to upload several files at once, consider wrapping them in a zip file.
To upload the file, you must perform a multipart upload, and include the file contents in the file field.
from felt_python import (refresh_file_layer, refresh_url_layer)
refresh_file_layer(map_id, layer_id, file_name="features.geojson")
refresh_url_layer(map_id, layer_id)from felt_python import upload_file
upload_file(Enabling Layer Export
You can allow EmbedToken based page views to export layer data.
Turn on "Viewer permissions: Export data" in Map settings
<iframe src="https://felt.com/embed/map/{mapId}?token={token}"></iframe