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...
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 load a Felt map into their application while providing opportunities to hook into the interaction loop and more broadly customize the experience of the map viewer.
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="<YOUR_API_TOKEN>"
curl -L \
-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
# Your API token should look like this:
# api_token = "felt_pat_ABCDEFUDQPAGGNBmX40YNhkCRvvLI3f8/BCwD/g8"
api_token = "<YOUR_API_TOKEN>"
r = requests.post(
"http://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,
public_access="private",
)
map_id = response["id"]
Notice in the response the "id"
property. Every map has a unique ID, which is also a 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.
curl -L \
-H "Authorization: Bearer ${FELT_API_TOKEN}" \
"https://felt.com/api/v2/maps/${MAP_ID}"
r = requests.get(
f"http://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)
To remove a map from your workspace, simply perform a DELETE
request to the map's URL:
curl -L \
-X DELETE \
-H "Authorization: Bearer ${FELT_API_TOKEN}" \
"https://felt.com/api/v2/maps/${MAP_ID}"
r = requests.delete(
f"http://felt.com/api/v2/maps/{map_id}",
headers={"Authorization": f"Bearer {api_token}"}
)
assert r.ok
from felt_python import delete_map
delete_map(map_id)
To move a map to a different folder or project, send a POST request to the map's move URL:
curl -L \
-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"http://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)
Creates a short-lived (15 minutes) token 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.
Generate a token by making a call to this API from your server
Securely pass the token to your frontend client
Include the token as a query parameter on the Embed URL in an iframe
<iframe src="https://felt.com/embed/map/{mapId}?token={token}"></iframe>
You can allow EmbedToken based page views to export layer data.
Turn on "Viewer permissions: Export data" in Map settings
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)
An array of URLs to import on map creation
Returns details including title, URL, thumbnail URL, creation and visited timestamps.
Update the title, description and access permissions of a map.
Move a map to a different project or folder. Project IDs and Folder IDs can be found inside map settings.
Duplicate a map and all of its contents.
List projects that you have access to.
Returns details of a project including name, visibility, id and a list of references to the maps in the project.
Create a new project with the provided name and visibility.
Update a project's name or visibility.
Delete a project and all of it's contents.
Caution: Deleting a project deletes all of the folders and maps inside!
The Felt SDK allows you to embed, connect to and ultimately control Felt map embeds, enabling you to build powerful, interactive custom applications around Felt.
You are able to control many aspects of the Felt UI and map contents, as well as being notified of events happening in the map such as clicks, selections, etc.
This feature is available to customers on the Enterprise plan. Reach out to set up a trial.
Assuming you are using a modern JavaScript environment, install the SDK using your preferred package manager:
npm install @feltmaps/js-sdk
Create an HTML page with a container element:
<html>
<body>
<div id="container"></div>
</body>
</html>
Embed a Felt map in your container element and use the SDK to control it:
import { Felt } from "@feltmaps/js-sdk";
const map = await Felt.embed(
document.querySelector("#container"),
"FELT_MAP_ID",
);
const layers = await map.getLayers();
const elements = await map.getElements();
For more information on how to control a map, see Controlling maps.
If you are building a React application, you can use the Felt SDK React Starter Repo to get started quickly.
You can also read our guide on Integrating with React to learn more about how to use the Felt SDK with React.
The Felt SDK provides several methods for reading data from your map's entities (layers, elements, etc.) and staying in sync with their changes. This guide will show you how to read entity data and react to changes.
The Felt SDK provides both singular and plural versions of getter methods for various entity types. This allows you to retrieve either a single entity or multiple entities of the same type.
For example, for layers:
// Get a single layer
const layer = await felt.getLayer("layer-1");
// Get all layers
const layers = await felt.getLayers();
All of the methods that return multiple entities accept constraint parameters to filter the results:
// Get elements with specific IDs
const elements = await felt.getElements({
ids: ["element-1", "element-2"]
});
// Get legend items belonging to a layer
const legendItems = await felt.getLegendItems({
layerIds: ["layer-1"],
});
To stay in sync with entities, use the appropriate on[EntityType]Change
method. For example, to monitor layer changes:
felt.onLayerChange({
options: {id: "layer-1"},
handler: ({layer}) => {
console.log(layer.visible);
}
});
Cleanup: Always store and call the unsubscribe functions when you're done listening for changes so that the listener doesn't continue to run in the background.
Error handling: When getting entities, remember that the methods may return null
if the entity doesn't exist:
const layer = await felt.getLayer("layer-1");
if (layer) {
// Layer exists, safe to use
console.log(layer.visible);
} else {
// Layer not found
console.log("Layer not found");
}
Batch operations: When you need multiple entities, use the bulk methods (getLayers
, getElements
, etc.) with constraints rather than making multiple individual calls:
// 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");
This approach to reading and monitoring entities gives you full control over your map's data and allows you to build interactive applications that stay in sync with the map's state.
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
"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
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]
Expansion 1: id
is equal (”eq”
) to one of 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
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
"filters": ["acres", "lt", 50000]
"filters": [["acres", "ge", 50000], "and", ["acres", "le", 70000]]
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, Elements, Sources, Projects and more.
This feature is available to customers on the Enterprise plan. Reach out to set up a trial.
All Felt API endpoints are hosted at the following base URL:
https://felt.com/api/v2
All calls to the Felt API must be authenticated. The easiest way to authenticate your API calls is by creating a API token and providing it as a Bearer
token in the request header.
You can create an API token in the Developers tab of the Workspace Settings page:
Learn more about API tokens here:
The easiest way to interact with the Felt API is by using our felt-python
SDK. You can install it with the following command:
pip install felt-python
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 -L \
-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(
"http://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,
public_access="private",
)
map_id = response["id"]
Notice in the response the "id"
property. Every map has a unique ID, which is also a 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.
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 -L \
-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"http://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 (also called "id"
in the response) so we can style it in the next call.
You can see the uploaded result in your map:
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="CjU1CMJPTAGofjOK3ICf1D"
LAYER_ID="<YOUR_LAYER_ID>"
curl -L \
-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.1"}}'
new_fsl = {
"paint": {
"color": "green",
"opacity": 0.9,
"size": 30,
"strokeColor": "auto",
"strokeWidth": 1
},
"legend": {},
"type": "simple",
"version": "2.1"
}
r = requests.post(
f"http://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.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:
A layer must have finished uploading successfully before it can be refreshed
Similar to a URL upload, refreshing an existing URL layer is just a matter of making a single POST
request:
curl -L \
-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 occured!
All calls to the Felt API require authorization using a Bearer
token in the request header:
Authorization: Bearer <API Token>
These are tokens associated to your account only, and that you have to manually provide to the application you want to use.
Since these tokens grant access to your account, you must store them securely and treat them as a password to your account.
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.
Once you have your API token, you can authenticate your requests to the Felt API by using it as a bearer token in your Authorization
header:
Authorization: Bearer felt_pat_07T+Jmpk...
Here's an example showing how to create a new Felt map:
# This looks like:
# FELT_API_TOKEN="felt_pat_ABCDEFUDQPAGGNBmX40YNhkCRvvLI3f8/BCwD/g8"
FELT_API_TOKEN="<YOUR_API_TOKEN>"
curl -L \
-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(
"http://felt.com/api/v2/maps",
json={"title": "My newly created map"},
headers={"Authorization": f"Bearer {api_token}"}
)
assert r.ok
print(r.json())
Returns a GeoJSON FeatureCollection containing all the elements in a map that are not in an element group
Returns a GeoJSON FeatureCollection containing all the elements in a single group
Returns a list of GeoJSON Feature Collections, one for each element group
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.
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.
You may assign Elements to an Element Group by setting the felt:parentId
property of an Element to the ID of an Element Group.
Deletes the element with the ID specified in the path.
Returns an export of the map comments.
The export can be generated in JSON or CSV format, you can specify the format with the format
query parameter, which can be either json
or csv
. It defaults to json
.
Resolve an individual comment based on it's ID.
Manage your Cloud Data Sources with Felt REST API.
Lists Sources that you have access to. To see the datasets in an individual Source, fetch it from the Show Source endpoint.
Fetches a single Source and all the datasets it contains.
Triggers the creation of a new Source. Each connection type has it's own set of required parameters, depending on what kind of Source it is. These are provided in the connection
field.
Connecting the Source and inspecting it's 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
.
Triggers Felt to reconnect to your Source and re-inspect all the datasets it contains. 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
.
Updates details of a Source. Connecting the Source and inspecting it's 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
.
Deletes a Source.
Any layers created from the Source will remain after it is deleted, but they will no longer 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:
<html>
<body>
<h1>My Felt app</h1>
<div id="container"></div>
</body>
</html>
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:
import { Felt } from "@feltmaps/js-sdk";
const map = await Felt.embed(
document.querySelector("#container"),
"FELT_MAP_ID",
);
// Now use the SDK
const layers = await map.getLayers();
const elements = await map.getElements();
// You also have a reference to the iframe itself:
map.iframe.style.width = "50%";
Felt.embed
to mount into an existing iframeIn 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:
<html>
<body>
<h1>My Felt app</h1>
<iframe id="my-iframe"></iframe>
</body>
</html>
import { Felt } from "@feltmaps/js-sdk";
const map = await Felt.embed(
document.querySelector("#my-iframe"),
"FELT_MAP_ID",
);
Felt.connect
to connect to an existing embedded Felt mapThere 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:
<html>
<body>
<h1>My Felt app</h1>
<iframe src="https://felt.com/map/example-map-123" id="my-iframe"></iframe>
</body>
</html>
import { Felt } from "@feltmaps/js-sdk";
const map = await Felt.connect(
document.querySelector("#my-iframe").contentWindow
);
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 Style Language is a way for advanced users to style data quickly, through simple JSON code. This document is a guide to use as you test data files in Felt. For an exhaustive guide to all supported properties see the Reference Documentation.
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 and hillshade.
Details on how to customize legends on a per-layer basis.
Definitions for errors raised when validating the Felt Style Language.
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:
{
"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"],
"visible": true
},
"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"
}
This is an example of a simple visualization using a raster dataset
and is defined by the following style:
{
"version": "2.3",
"type": "simple",
"config": {},
"paint": {"isSandwiched": false, "opacity": 0.93}
}
Heatmaps are used to visualize the density of points on a map.
Heatmaps visualizations are defined using “type”: “heatmap”
and, allow the following properties to be set:
color
An array of colors that will be used in the heatmap. From less density to more.
size
Controls the size of each point.
intensity
Controls the intensity of a heightmap.
This is an example of a heatmap visualization
defined with the following visualization
{
"version": "2.3",
"type": "heatmap",
"config": {},
"legend": {"displayName": {"0": "Low", "1": "High"}},
"paint": {"color": "@purpYlPink", "size": 10, "intensity": 0.2}
}
Heatmaps do not support labels.
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 :
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:
You can check the upload status of a layer by querying it:
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 elements, comments or data layers on a fixed interval, a better alternative is to set up a webhook where Felt will send a notification any time a map is updated. This allows you to build integrations on top, such as sending a Slack message or performing calculations for the newly-drawn area.
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 can set up webhooks in the .
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 layer 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.
The Felt SDK provides methods to read and react to selection changes in your map. Selection refers to the currently highlighted entities (elements, features, etc.) that the user has clicked on or selected through other means.
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 elements and features.
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.
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 element geometry and map interactions, process the returned data, and visualise isochrones as dynamic layers. View the app and code .
The legend block contains information on how the legend will be displayed for this visualization.
Take a look at to see how it works.
These are the fields that each legend block can contain:
Most of the properties used on the style
and label
blocks can be defined using interpolators to enable zoom-based styling. Take a look at the full list of properties that can be interpolated in .
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 page.
An example of a layer changing feature colors depending on the zoom level can be found below
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.
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.
Attribute ‘dashArray’
has to be a two-numbers array.
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
.
Attribute 'showOther'
must be one of above
, 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 .
Hillshades are used to visualize the valleys and ridges encoded in elevation raster data.
Hillshade visualizations are defined using “type”: “hillshade”
and, support the following properties:
The following map is an example of a raster layer using a hillshade visualization
with the following style
Felt also supports adding color to hillshades by defining a color
property in the style
which is defined by the following style
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:
const selection = await felt.getSelection();
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...
}
});
titleAttribute
Optional. The attribute that will be 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 either “table” or “list”. The way the popup will show its contents. Defaults to "table"
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 in the popup. If it’s defined, all attributes here will show even if the selected feature doesn’t include them.
"popup": {
"keyAttributes": [
"osm_id",
"barriers",
"highway",
"ref",
"is_in",
"place",
"man_made",
"other_tags"
],
"titleAttribute": "barriers",
"popupLayout": "list"
}
displayName
"legend": {
"displayName": {
"category-1": "Category 1",
"category-2": "Category 2"
}
}
"legend": {
"displayName": {
"0": "-0.5",
"1": "0.5"
}
}
{
"body": {
"attributes": {
"type": "map:update",
"updated_at": "2024-04-29T12:16:46",
"map_id": "Jzjr8gMKSrCOxZ1OSMT49CB"
}
}
}
"paint": {
"color": {"linear": [[14, "red"], [20, "blue"]]},
...
}
color
An array of colors that will be used in the hillshade. From less elevation to more.
source
The light angle. 0 is North, 90 East, …
intensity
Controls the intensity of a hillshade.
{
"version": "2.3",
"type": "hillshade",
"config": {"band": 1},
"legend": {},
"paint": {"isSandwiched": false}
}
{
"config": {"band": 1, "steps": [-154.46435546875, 7987.457987843631]},
"legend": {},
"type": "hillshade",
"version": "2.3",
"paint": {
"isSandwiched": false,
"color": "@feltHeat",
"source": 315,
"intensity": 0.76
}
}
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()
:
const viewport = await felt.getViewport();
console.log(viewport.center); // { latitude: number, longitude: number }
console.log(viewport.zoom); // number
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:
felt.setViewport({
center: {
latitude: 37.7749,
longitude: -122.4194
},
zoom: 12
});
Use fitViewportToBounds()
to adjust the viewport to show a specific rectangular area:
felt.fitViewportToBounds({
bounds: [
west, // minimum longitude
south, // minimum latitude
east, // maximum longitude
north // maximum latitude
]
});
To stay in sync with viewport changes, use the onViewportMove
method:
const unsubscribe = felt.onViewportMove({
handler: (viewport) => {
console.log("New center:", viewport.center);
console.log("New zoom:", viewport.zoom);
}
});
// Clean up when done
unsubscribe();
Listen for click events on the map using onPointerClick
:
const unsubscribe = felt.onPointerClick({
handler: (event) => {
// Location of the click
console.log("Click location:", event.center);
// Features under the click
console.log("Clicked features:", event.features);
}
});
Track mouse movement over the map using onPointerMove
:
const unsubscribe = felt.onPointerMove({
handler: (event) => {
// Current mouse location
console.log("Mouse location:", event.center);
// Features under the cursor
console.log("Hovered features:", event.features);
}
});
Cleanup: Always store and call unsubscribe functions when you're done listening for events:
const unsubscribe = felt.onPointerMove({
handler: (event) => {
// Handle event...
}
});
// Later, when you're done:
unsubscribe();
Throttling: For pointer move events, consider throttling your handler if you're doing expensive operations:
import { throttle } from "lodash";
felt.onPointerMove({
handler: throttle((event) => {
// Handle frequent mouse moves...
}, 100) // Limit to once every 100ms
});
By using these viewport controls and interaction handlers, you can create rich, interactive experiences with your Felt map.
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"]
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="YOUR_FILE_WITH_EXTENSION",
layer_name="My new layer",
)
# 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 library
curl -L \
"https://felt.com/api/v2/maps/${MAP_ID}/layers/{LAYER_ID}" \
-H "Authorization: Bearer ${YOUR_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"]
# 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 -L \
-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"}'
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>"
r = requests.post(
f"http://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"]
Categorical visualizations use a categorical attribute and the categories within it to apply styling to discrete categories of the attribute. On raster datasets, it uses a band instead of an attribute.
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.
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
{
"version": "2.3",
"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]]}]
}
}
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 "Nuclear"
, "Oil"
, "Coal"
, "Gas"
, "Wind"
, "Hydro"
and "Solar"
. 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
)
The Cropscape CDL layer in Felt is an example of a categorical layer on a raster dataset
and is defined by the following style:
{
"version": "2.3",
"type": "categorical",
"config": {
"categories": [
254,
250,
249,
248,
247,
...
4,
3,
2,
1,
0
],
"showOther": false
},
"legend": {
"displayName": {
"0": "Background",
"1": "Corn",
"2": "Cotton",
"3": "Rice",
"4": "Sorghum",
...
"247": "Turnips",
"248": "Eggplants",
"249": "Gourds",
"250": "Cranberries",
"254": "Dbl Crop Barley/Soybeans"
}
},
"paint": {
"color": [
"rgb(38, 113, 0)",
"rgb(252, 105, 101)",
"rgb(252, 105, 101)",
"rgb(252, 105, 101)",
"rgb(252, 105, 101)",
...
"RGB(255, 158, 15)",
"RGB(0, 169, 230)",
"RGB(255, 38, 38)",
"RGB(255, 212, 0)",
"rgba(0, 0, 0, 0)"
],
"isSandwiched": false,
"opacity": 0.93
}
}
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.
version
Mandatory. Defines which version this style adheres to
type
Optional. One of , , or . Defaults to simple.
config
Optional. A block that contains some configuration options to be used across the style. .
style
Optional. An object 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.
{
"version": "2.1",
"type": "simple",
"style": {...}
}
A layer's style is defined in a JSON-based called , or FSL for short. Editors can view the current style of a layer inside a Felt map by clicking on Actions > Edit styles
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:
To update a layer's style, we can send a POST
request with the new FSL to the same layer's /update_style
endpoint.
You can find examples of FSL for different visualization types in of these docs:
: same color and size for all features (vector) or pixels (raster).
: different color per feature or pixel, based on a categorical attribute
: different color or size per feature or pixel, based on a numeric attribute.
: a density-based visualization style, for vector point layers.
: a special kind of visualization for raster elevation layers.
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.
A layer must have finished uploading successfully before it can be refreshed
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
request:
This guide covers common patterns and concepts used throughout the Felt SDK.
By following these patterns consistently throughout the SDK, we aim to make the API predictable and easy to use while maintaining flexibility for future enhancements.
All methods in the Felt SDK are asynchronous and return Promises. 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 methods:
A singular getter for retrieving one entity by ID:
A plural getter that accepts constraints for retrieving multiple entities:
The plural getters allow you to pass no constraints, in which case they'll return all entities of that type:
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:
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:
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.
useFeltEmbed
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 map.getLayers()
:
The Felt SDK provides methods to control the visibility of various entities like layers, layer groups, element 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:
Control visibility of layer groups using setLayerVisibility
:
Control visibility of layer groups using setLayerGroupVisibility
:
Similarly, control element group visibility with setElementGroupVisibility
:
Legend items require both a layer ID and an item ID to identify them. Use setLegendItemVisibility
:
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:
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.
To see how a legend is defined, take a look at .
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)
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 elements = await felt.getLegendItems();
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)
})
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);
}
});
}
});
{
show?: string[], // IDs of entities to show
hide?: string[] // IDs of entities to hide
}
felt.setLayerVisibility({
show: ["layer-1", "layer-2"],
hide: ["layer-3"]
});
felt.setLayerGroupVisibility({
show: ["group-1", "group-2"],
hide: ["group-3"]
});
felt.setElementGroupVisibility({
show: ["points-group"],
hide: ["lines-group", "polygons-group"]
});
felt.setLegendItemVisibility({
show: [
{ layerId: "layer-1", id: "item-1" },
{ layerId: "layer-1", id: "item-2" }
],
hide: [
{ layerId: "layer-1", id: "item-3" }
]
});
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"]
});
{
"config": {"labelAttribute": ["type"]},
"legend": {},
"paint": {
"color": "blue",
"opacity": 0.9,
"size": 30,
"strokeColor": "auto",
"strokeWidth": 1
},
"type": "simple",
"version": "2.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 -L \
-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"http://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)
curl -L \
-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.1"}}'
new_fsl = {
"paint": {
"color": "green",
"opacity": 0.9,
"size": 30,
"strokeColor": "auto",
"strokeWidth": 1
},
"legend": {},
"type": "simple",
"version": "2.1"
}
r = requests.post(
f"http://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.1"
}
update_layer_style(
map_id=map_id,
layer_id=layer_id,
style=new_fsl,
)
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("wPV9BqMuYQxmUraVWy9C89BNA", {
uiControls: {
cooperativeGestures: false,
fullScreenButton: false,
showLegend: false,
},
});
return (
<div>
{/* the map container */}
<div ref={mapRef} />
{/* a component that uses the Felt controller */}
<MyFeltApp felt={felt} />
</div>
);
}
import {
Felt,
FeltController,
FeltEmbedOptions,
Layer,
LayerGroup,
} 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
React.useEffect(() => {
return felt.onLayerChange({
options: { id: initialLayer.id },
handler: ({ layer }) => setLayer(layer),
});
}, [initialLayer.id]);
// return the live layer
return currentLayer;
}
"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"
}
}
# This code is a continuation of the previous Python code block
# and assumes you already have a "presigned_upload" variable
url = presigned_upload["url"]
presigned_attributes = presigned_upload["presigned_attributes"]
# A 204 response indicates that the upload was successful
with open(YOUR_FILE_WITH_EXTENSION, "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 library
# 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 -L \
-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"http://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)
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"http://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
)
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.
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
Component filters: Set through interactive legend components
Ephemeral filters: Set temporarily through the SDK
You can inspect these different filter sources using getLayerFilters
:
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 filters
Use setLayerFilters
to apply ephemeral filters to a layer:
felt.setLayerFilters({
layerId: "layer-1",
filters: ["POPULATION", "gt", 1000000]
});
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)
See here for more details on filter operators.
You can combine multiple conditions using boolean operators:
felt.setLayerFilters({
layerId: "layer-1",
filters: [
["POPULATION", "gt", 1000000],
"and",
["COUNTRY", "eq", "USA"]
]
});
Here's a common use case where we filter a layer to show only features that match a property of a selected feature:
// 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
});
}
}
});
Clear filters: Set filters to null
to remove them entirely:
felt.setLayerFilters({
layerId: "layer-1",
filters: null
});
Check existing filters: Remember that your ephemeral filters combine with existing style and component filters:
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");
}
Type safety: Use TypeScript to ensure your filter expressions are valid:
import type { Filters } from "@feltmaps/sdk";
const filter: Filters = ["POPULATION", "gt", 1000000];
felt.setLayerFilters({
layerId: "layer-1",
filters: filter
});
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:
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);
}
To create a layer from a GeoJSON file on the user's device:
// 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;
}
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:
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"
});
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:
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
}
}
}
});
Each style should be a valid FSL (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:
await felt.deleteLayer("layer-1");
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.
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"
});
Manual Refresh: Simply replace the source
property of any layer you have created, using the method to update the source data.
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.
{ "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
The following image shows the behavior of this definition:
{ "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": Stops[] }
: Linearly interpolates between stop values less than or equal and greater than the input value
{
"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
The following image shows the behaviour of this definitions
{ "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": [number, number] }
: Expands to { "linear": [[minZoom, number], [maxZoom, number] }
{ "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
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)
{
"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
The following images shows the behaviour of this definition
{ "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": [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
{ "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]]} // Yellow
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
Optional. A object that encodes how numeric fields should be shown.
"attributes": {
"faa": {
"displayName": "FAA Code",
"format": {
"mantissa": 0,
"thousandSeparated": true
}
},
"wikipedia": {"displayName": "Wikipedia"},
}
Elements live at the top layer of a map, and are created directly inside the Felt app.
Elements live at the top layer of a map, and are created directly inside the Felt app.
Elements are returned as a .
Returns a list of GeoJSON Feature Collections, one for each element group.
Each element is represented by a feature in the POST
ed GeoJSON Feature Collection.
For each feature, including an existing element ID (felt: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.
Elements can be deleted by referencing them by ID
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 will include a target URL and some pre-signed attributes, which will be used to upload the new file to Amazon S3.
This endpoint is used to create a layer, and obtain a pre-signed url to upload the layer files to S3.
Layer files aren’t uploaded directly to the Felt API. Instead, they are uploaded by your client directly to an S3 bucket.
You will receive a single set of URL and pre-signed params to upload the 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 using the pre-signed params, you must perform a multipart upload, and include the file contents in the file
field:
After uploading a file or URL, you may want to update the resulting layer with some new data. The process is quite similar to the above:
For URL uploads, simply making a single POST
request to the refresh endpoint is enough
For file uploads, the response of the initial POST
request will include a URL and some presigned attributes, which will be used to upload the new file to Amazon S3. See for more details.
A layer's style may be updated by providing a new Felt Style Language object. Learn more in the guide:
Get the details of all the layer on the map that are not within a layer group.
Get the details of a single layer on the map. These details include:
Name of the layer
Upload status and progress
Style, expressed in the
Other metadata, such as the geometry type, visibility state in the legend, etc
Update a layer's name or move it into or out of a layer group.
To move a layer into a group, set the layer_group_id
to the id
of the layer group you want to move the layer into. To move a layer out of a group, set the layer_group_id
to null
.
Provide an array of layer group objects to create new groups or update existing ones.
For each layer group object, including an existing ID will result in the group's details (name, subtitle and legend order) being updated. If no layer group ID is provided (or a non-existent one is provided), a new layer group will be created.
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 and the layer ID provided by this endpoint.
Publish a layer to your workspace's library
Publish a layer group to your workspace's library
Get a link to export a layer as a GeoPackage (vector layers) or GeoTIFF (raster layers)
Create an export request of a layer as a GeoPackage, GeoJSON, or CSV. Optionally include filters with the layer. Export requests are asynchronous. A successful response will return a poll_endpoint to check the status of the export.
Check the status of an Custom Export. If successful, the response will include a download_url
Duplicate an array of layers or layer groups to a map
{
"version": "2.3",
"type": "simple",
"config": {},
"style": {},
"label": {}
}
{
"version": "2.3",
"type": "simple",
"config": { "labelAttribute": ["oper_cln", "owner_cln"] },
"paint": {
"color": "#8F7EBF",
"strokeColor": "#CEC5E8"
},
"label": {
"haloColor": "#E9E4F7",
"color": "#8F7EBF"
}
}
{
"version": "2.3",
"type": "simple",
"config": { "labelAttribute": ["name"] },
"label": {
"minZoom": 4,
"color": "#804779",
"haloColor": "#EBD3E8",
"haloWidth": 1,
"fontSize": [12, 21]
}
}
{
"version": "2.3",
"type": "simple",
"config": { "labelAttribute": ["WSR_RIVER_"] },
"style": {
"color": "hsl(217, 80%, 40%)"
},
"label": {
"color": "hsl(217, 80%, 40%)",
"fontStyle": "italic",
"repeatDistance": 200
}
}
{
"version": "2.3",
"type": "categorical",
"config": {
"categoricalAttribute": "primary_fu",
"categories": ["Oil", "Coal", "Gas", "Hydro", "Wind", "Solar"]
},
"style": {
"color": [
"#EB9360",
"#AD7B68",
"#A4B170",
"#7AB6C2",
"#8F99CC",
"#E5C550"
],
"strokeColor": [
"#FFC8A8",
"#D4D4D4",
"#CBD79D",
"#A3D6E0",
"#BCC3E5",
"#F2DB85"
],
"size": [[1.5, 6]],
"strokeWidth": [[0.25, 2]]
},
"label": {
"minZoom": 12,
"placements": ["E", "W"],
"color": [
"#DE7D45",
"#946E59",
"#7E8C46",
"#5B99A6",
"#6270B2",
"#CCA929"
],
"haloColor": [
"#FAEAE1",
"#F2E9E4",
"#EDF2DA",
"#D8ECF0",
"#E4E7F7",
"#F2E8C2"
],
"lineHeight": [1.2],
"fontSize": [
{
"linear": [
[12, 10],
[20, 20]
]
}
]
}
}
{
"version": "2.3",
"type": "numeric",
"config": {
"numericAttribute": "Renter occupied (%)",
"steps": [5, 20, 25, 35, 45, 100]
},
"legend": {"displayName": "auto"},
"paint": {
"color": "@galaxy",
"opacity": 0.9,
"strokeColor": ["#9e9e9e"],
"strokeWidth": 0.5,
"isSandwiched": true
}
}
{
"version": "2.3",
"type": "heatmap",
"config": {},
"legend": {"displayName": {"0": "Low", "1": "High"}},
"paint": {
"color": "@purpYlPink",
"highlightColor": "#EA3891",
"highlightStrokeColor": "#EA3891",
"highlightStrokeWidth": {"linear": [[3, 0], [20, 2]]},
"isSandwiched": false,
"opacity": 0.9,
"size": 10,
"strokeColor": "#8F7EBF",
"strokeWidth": {"linear": [[3, 0.8], [20, 2]]},
"intensity": 0.2
}
}
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 -L \
-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",
"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"
}
}
]
}
r = requests.post(
f"http://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 -L \
-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"http://felt.com/api/v2/maps/{map_id}/elements/{element_id}",
headers={"Authorization": f"Bearer {api_token}"},
)
assert r.ok
from felt_python import delete_element
element_id = "<YOUR_ELEMENT_ID>"
delete_element(map_id, element_id)
# 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 -L \
-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"http://felt.com/api/v2/maps/{map_id}/elements",
headers={"Authorization": f"Bearer {api_token}"}
)
assert r.ok
print(r.json())
curl -L \
-H "Authorization: Bearer ${FELT_API_TOKEN}" \
"https://felt.com/api/v2/maps/${MAP_ID}/element_groups"
r = requests.get(
f"http://felt.com/api/v2/maps/{map_id}/elements",
headers={"Authorization": f"Bearer {api_token}"}
)
assert r.ok
print(r.json())
from felt_python import list_element_groups
list_element_groups(map_id)
The config block contains configuration options for a given visualization.
These are the fields that each config block can contain:
band
Optional. Used in raster numeric visualizations. The raster band that we’ll use to get the data from.
categoricalAttribute
Mandatory for categorical visualizations. The attribute that contains the categorical attributes that will be used.
categories
Mandatory for a categorical visualization. An array of the values that will be used as categories. Categories will be rendered from top to bottom following the definition order.
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 raster algebra numeric visualizations. The type of operation and the bands used for it.
noData
Optional. Used in raster visualizations. Defines values that won’t be shown
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”.
rasterResampling
Optional. Used on raster data visualizations. It can be set to either “nearest” or “linear”. Defaults to “nearest”
showOther
Optional. Used in categorical visualizations. If this field is set to true it will show all features that do not match any of the defined categories and add an extra entry as the last item in the legend.
steps
Mandatory for a numeric visualization. An array of values that are either step based depending on the classification method and number of steps chosen or, for a continuous visualization, the min and max values from the numericAttribute.
rasterResampling
-
-
-
“nearest”
highlightColor
"#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
-
-
"config": [
{
"labelAttribute": ["Wikipedia", "faa"],
"categoricalAttribute": "faa",
"categories": ["faa-code-1", "faa-code-2", "faa-code-3"],
"showOther": true,
"otherOrder": "above"
}
]
"config": [
{
"labelAttribute": ["Wikipedia", "faa"],
"numericAttribute": "percentage",
"steps": [1, 25, 50, 75, 100]
}
]
"config": {
"band": 1,
"method": {"NDVI": {"NIR": 1, "R": 2}},
"steps": [-0.5, 0.5]
},
# Requires requests library to be installed with: pip install requests
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>"
path_to_file = "<YOUR_FILE_WITH_EXTENSION>" # Example: features.geojson
# Request a pre-signed URL from Felt
layer_response = 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"},
)
presigned_upload = layer_response.json()
url = presigned_upload["data"]["attributes"]["url"]
presigned_attributes = presigned_upload["data"]["attributes"]["presigned_attributes"]
# A 204 response indicates that the upload was successful
with open(path_to_file, "rb") as file_obj:
output = requests.post(
url,
# Order is important, file should come at the end
files={**presigned_attributes, "file": file_obj},
)
from felt_python import upload_file
map_id = "<YOUR_MAP_ID>"
path_to_file = "<YOUR_FILE_WITH_EXTENSION>" # Example: features.geojson
upload_file(
map_id=map_id,
file_name=path_to_file,
layer_name="My new layer",
)
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.
<!doctype html>
<html lang="en">
<head>
<title>Felt JS SDK</title>
</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 unpkg 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
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);
});
// a helper function to toggle the visibility of a layer
async function setLayerVisibility(previousVisibility, layer) {
if (previousVisibility) {
felt.setLayerVisibility({ hide: [layer.id] });
} else {
felt.setLayerVisibility({ show: [layer.id] });
}
// update the layer's visibility state
layer.visible = !previousVisibility;
}
// 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>
`;
// add an event listener to the checkbox to toggle the layer visibility
layerElement
.querySelector("input")
.addEventListener("change", () =>
setLayerVisibility(layer.visible, layer),
);
// add the layer element to the container
layerContainer.appendChild(layerElement);
});
});
</script>
</body>
<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>
</html>
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.
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)
// 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", "gte", 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 }
]
*/
Comparing building heights across time periods (Histograms)
// 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", "gte", 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 }
]
*/
Comparing neighborhood density (Aggregates)
// 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 }
Here's how you might integrate these analysis methods with an interactive chart:
// 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} building age: ${2025 - stats.avg}`);
}
// Initialize the chart when the page loads
createBuildingTypePieChart();
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.
The Felt SDK provides two main approaches for creating elements on your maps:
Interactive Drawing: Configure and activate drawing tools for users to create elements manually
Programmatic Creation: Create and modify elements directly through code
Elements 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 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 elements with the tools, you can be notified of them being created and updated using the and listeners. See Listening for element creation for more details.
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
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.
// 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);
If you want to create elements programatically instead of letting your users draw them interactively on the map, use the methods in the .
To create elements, use the method.
To update elements, use the method.
To delete elements, use the method.
When elements are created programatically, they also trigger notifications about the corresponding changes to elements, via onElementCreate
, onElementChange
and onElementDelete
.
// 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)
Extract the geometric representation of elements using the method.
The geometry is returned in GeoJSON geometry format, which can be quite different to the way the element is specified in Felt. For example, Circle
elements in Felt have their geometry converted into a polygon, representing the area covered by the circle.
Note: Text, Note, and Image elements do not return geometry as they are considered to annotations rather than true "geospatial" elements.
// Get an element's geometry in GeoJSON format
const geometry = await felt.getElementGeometry("element-1");
console.log(geometry?.type, geometry?.coordinates);
Every change that is made to the elements on a map results in a call to either , or .
// Set up a listener for changes to a polygon
const unsubscribeChange = felt.onElementChange({
options: { id: polygon.id },
handler: ({element}) => {
console.log("Polygon was updated:", element);
}
});
// Set up a listener for deletion
const unsubscribeDelete = felt.onElementDelete({
options: { id: polygon.id },
handler: () => {
console.log("Polygon was deleted");
}
});
// Later, clean up listeners
unsubscribeChange();
unsubscribeDelete();
There are two different ways for listening to elements being created, and the one you use depends on how the element is being created, and at what point you want to know about an element's creation.
When the user is creating elements 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 element (e.g. the polygon was closed or the marker stroke ended) then you should use the listener.
When elements are created programatically, they do not trigger the event.
Elements created using Tools or will trigger the event, with an extra property stating whether the element is still being created.
// Listen for any element creation
const unsubscribe = felt.onElementCreate({
handler: (element) => {
console.log(`New element created with ID: ${element.id}`);
// Check if the element is still being drawn
if (element.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();
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 elements and have them sent to your own backend systems for persistence or analysis.
Assuming you have embedded your Felt map as described in Getting started, and in your own UI you have added a polygon-tool
button and a reset-tool
button, all you need is the following:
// 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);
}
});
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 between 3-10 discrete steps using a classification method or on a continuous scale between an attribute’s min and max values. Felt offers the following methods to symbolize numeric data:
Jenks Natural Breaks
Finds natural groupings in attribute values to minimize differences within steps and maximize differences between them. A good option for clustered data. This is the default method in Felt.
Equal interval
Divides attribute values into an equal number of steps. Equal interval is a good option when data is equally spread across the range of values.
Quantiles
Places an equal number of values into each step. A good option for evenly distributed data.
Mean Standard Deviation
Steps are calculated based on an attribute values distance from the mean. A good option for data that follows a near-normal distribution.
Manual
Manually define which values to include in each size or color step. A good option when you know your data well.
Continuous
Attribute values are sized or colored based on a continuous scale between the min and max values versus discrete steps. A good option for continuous data.
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.
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 color
array where each color is applied to a distinct step
range
{
"config": {
"numericAttribute": "Renter occupied (%)",
"steps": {"type": "jenks", "count": 5}
},
"label": {
"color": ["#d2e2a6", "#9dc596", "#6ba888", "#3e897b", "#096b6d"],
"fontSize": 14,
"fontStyle": "Normal",
"fontWeight": 400,
"haloColor": ["#9e9e9e", "#9e9e9e", "#9e9e9e", "#9e9e9e", "#9e9e9e"],
"haloWidth": 1.5,
"justify": "auto",
"letterSpacing": 0.1,
"lineHeight": 1.3,
"maxLineChars": 10,
"maxZoom": 23,
"minZoom": 1,
"padding": 20,
"placement": ["Center"],
"visible": true
},
"legend": {"displayName": "auto"},
"paint": {
"color": "@galaxy",
"highlightColor": "hsla(329,81%,64%, 0.5)",
"highlightStrokeColor": "hsla(329,81%,64%, 0.8)",
"highlightStrokeWidth": {"linear": [[3, 0.5], [20, 2]]},
"isSandwiched": true,
"opacity": 0.9,
"showAboveBasemap": true,
"strokeColor": ["#9e9e9e", "#9e9e9e", "#9e9e9e", "#9e9e9e", "#9e9e9e"],
"strokeWidth": 0.5
},
"type": "numeric",
"version": "2.3"
}
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, the numericAttribute: PRECIP
has steps
that range between the min and max values of [2.5,125]
The color
property has an array of three colors ["#fde89b", "#f37b8a", "#4d53b3"]
that are interpolated to give each precipitation value a unique color
{
"config": {
"numericAttribute": "PRECIP",
"steps": {"count": 1, "type": "continuous"}
},
"label": {
"color": ["#fde89b", "#f37b8a", "#4d53b3"],
"fontSize": 14,
"fontStyle": "Normal",
"fontWeight": 400,
"haloColor": ["#9e9e9e", "#9e9e9e", "#9e9e9e"],
"haloWidth": 1.5,
"justify": "auto",
"letterSpacing": 0.1,
"lineHeight": 1.3,
"maxLineChars": 10,
"maxZoom": 23,
"minZoom": 1,
"padding": 20,
"placement": ["Center"],
"visible": true
},
"legend": {"displayName": "auto"},
"paint": {
"color": "@purpYl",
"highlightColor": "hsla(329,81%,64%, 0.5)",
"highlightStrokeColor": "hsla(329,81%,64%, 0.8)",
"highlightStrokeWidth": {"linear": [[3, 0.5], [20, 2]]},
"isClickable": false,
"isSandwiched": true,
"opacity": 0.9,
"strokeColor": ["#9e9e9e", "#9e9e9e", "#9e9e9e"],
"strokeWidth": 0
},
"type": "numeric",
"version": "2.3"
}
Felt also supports stepped and continuous color numeric visualizations on raster datasets like you can see on the map below
which includes a raster layer with the following style
{
"version": "2.3"
"type": "numeric",
"config": {"band": 1, "steps": [-154.46435546875, 7987.457987843631]},
"legend": {"displayName": {"0": "-154.46", "1": "7.99K"}},
"paint": {
"isSandwiched": false,
"opacity": 1,
"color": [
"#454b9f",
"#2d79a4",
"#18a2a9",
"#8cc187",
"#e5d96c",
"#eab459",
"#ef8b45",
"#e66250",
"#db2d5e"
]
}
}
Notice that in the config
block we are using band
instead of numericAttribute
to define which band will be used for display
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
{
"version": "2.3",
"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
}
}
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]
{
"version": "2.3",
"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
}
}
These are the properties available to define label rendering. Anchors can be lines or points, polygons are not supported.
See of these attributes on each label type.
If using a categorical
or numeric
visualization, all the previous properties must 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 .
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 | oblique
Points and lines
Optional. The font style
fontWeight
number
Points and lines
Optional. The font weight
haloColor
string | Interpolator
Points and lines
Optional. The label halo color
haloWidth
string | Interpolator
Points and lines
Optional. The label halo color width
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
minZoom
number
Points and lines
Optional. The minimum zoom level at which the label will be shown
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
TextPlacements[] | auto | LineLabelPlacement
Points and Lines
Optional. If defined on a points dataset, an array of label placements to test when placing the label. If all the placements collide with already existing labels, the label is not shown. If defined on a lines dataset, the label placement on a line
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
capitalize | 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"
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
23
23
23
minZoom
23
23
23
offset
[8, 8]
0
-
padding
2
1
0
placements
“auto”
“Above”
“Center”
repeatDistance
-
250
-
textTransform
"none"
"none"
"none"
Properties common to all visualization types.
isClickable
boolean
true
Optional. A flag to tell if features should be clickable
isHoverable
boolean
false
Optional. A flag to tell if features should be hoverable
isSandwiched
boolean
true
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
22
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 render 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. Specify and MapLibre paint
or layout
properties. See for more.
The following properties are available for the simple
type of visualization
color
string |
Points, lines and polygons
Optional. The color to be used
dashArray
number[]
Lines
Optional. The dash line definition
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
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
See default values for the default values of these attributes on each geometry type.
The following properties are available for the categorical
and numerical
type visualizations. (You can see an example of a categorical
visualization here)
color
string |
Points, lines and polygons
Optional. The color to be used
dashArray
number[]
Lines
Optional. The dash line definition
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
These are the properties available to define label rendering. Anchors can be lines or points, polygons are not supported.
color
string | auto |
Points and lines
Optional. The label color
fontFamily
string
Points and lines
Optional. The font family to use
fontSize
number |
Points and lines
Optional. The font size in pixels
fontStyle
normal | italic | oblique
Points and lines
Optional. The font style
fontWeight
number
Points and lines
Optional. The font weight
haloColor
string |
Points and lines
Optional. The label halo color
haloWidth
string |
Points and lines
Optional. The label halo color width
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 |
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
minZoom
number
Points and lines
Optional. The minimum zoom level at which the label will be shown
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
TextPlacements[] | auto | LineLabelPlacement
Points and Lines
Optional. If defined on a points dataset, an array of label placements to test when placing the label. If all the placements collide with already existing labels, the label is not shown. If defined on a lines dataset, the label placement on a line
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
capitalize | 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
See default values of these attributes on each label type.
If using a categorical
or numeric
visualization, all the previous properties must 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 here.
color
"#EE4D5A"
"#826DBA"
"#4CC8A3"
highlightColor
"#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
-
setTool
ElementsController
Each token must be associated with the email address of the user who will use it.
POST /api/v2/maps/{map_id}/embed_token HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
{
"expires_at": "2024-05-25T15:51:34",
"token": "text"
}
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.
POST /api/v2/maps HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
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": [
{
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"links": {
"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",
"ordering_key": 1,
"progress": 1,
"refresh_period": "15 min",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
],
"links": {
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6D"
},
"name": "text",
"ordering_key": 1,
"type": "layer_group",
"visibility_interaction": "default"
}
],
"layers": [
{
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"links": {
"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",
"ordering_key": 1,
"progress": 1,
"refresh_period": "15 min",
"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"
}
The ID of the map to delete
DELETE /api/v2/maps/{map_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
No content
GET /api/v2/maps/{map_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
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": [
{
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"links": {
"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",
"ordering_key": 1,
"progress": 1,
"refresh_period": "15 min",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
],
"links": {
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6D"
},
"name": "text",
"ordering_key": 1,
"type": "layer_group",
"visibility_interaction": "default"
}
],
"layers": [
{
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"links": {
"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",
"ordering_key": 1,
"progress": 1,
"refresh_period": "15 min",
"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"
}
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".
The new title for the map
POST /api/v2/maps/{map_id}/update HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
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": [
{
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"links": {
"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",
"ordering_key": 1,
"progress": 1,
"refresh_period": "15 min",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
],
"links": {
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6D"
},
"name": "text",
"ordering_key": 1,
"type": "layer_group",
"visibility_interaction": "default"
}
],
"layers": [
{
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"links": {
"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",
"ordering_key": 1,
"progress": 1,
"refresh_period": "15 min",
"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"
}
POST /api/v2/maps/{map_id}/move HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
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": [
{
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"links": {
"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",
"ordering_key": 1,
"progress": 1,
"refresh_period": "15 min",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
],
"links": {
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6D"
},
"name": "text",
"ordering_key": 1,
"type": "layer_group",
"visibility_interaction": "default"
}
],
"layers": [
{
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"links": {
"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",
"ordering_key": 1,
"progress": 1,
"refresh_period": "15 min",
"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"
}
The ID of the map to duplicate
Title for the duplicated map. If not provided, will default to '[Original Title] (copy)'
POST /api/v2/maps/{map_id}/duplicate HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
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": [
{
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"links": {
"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",
"ordering_key": 1,
"progress": 1,
"refresh_period": "15 min",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
],
"links": {
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6D"
},
"name": "text",
"ordering_key": 1,
"type": "layer_group",
"visibility_interaction": "default"
}
],
"layers": [
{
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"links": {
"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",
"ordering_key": 1,
"progress": 1,
"refresh_period": "15 min",
"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"
}
Only needed when using the API as part of a plugin
GET /api/v2/projects HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
[
{
"id": "luCHyMruTQ6ozGk3gPJfEB",
"links": {
"self": "https://felt.com/api/v2/projects/V0dnOMOuTd9B9BOsL9C0UjmqC"
},
"name": "text",
"type": "project_reference",
"visibility": "workspace"
}
]
GET /api/v2/projects/{project_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
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"
}
],
"name": "text",
"type": "project",
"visibility": "workspace"
}
The name to be used for the Project
Either viewable by all members of the workspace, or private to users who are invited.
POST /api/v2/projects HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Accept: */*
Content-Length: 40
{
"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"
}
],
"name": "text",
"type": "project",
"visibility": "workspace"
}
The ID of the project to update
The name to be used for the Project
Either viewable by all members of the workspace, or private to users who are invited.
POST /api/v2/projects/{project_id}/update HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Accept: */*
Content-Length: 40
{
"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"
}
],
"name": "text",
"type": "project",
"visibility": "workspace"
}
The ID of the Project to delete. Note: This will delete all Folders and Maps inside the project!
DELETE /api/v2/projects/{project_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
No content
The ID of the map to list elements from.
GET /api/v2/maps/{map_id}/elements HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
{
"features": [
{
"geometry": {
"felt:id": "luCHyMruTQ6ozGk3gPJfEB",
"felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
},
"properties": {},
"type": "Feature"
}
],
"type": "FeatureCollection"
}
The ID of the map.
The ID of the element group.
GET /api/v2/maps/{map_id}/element_groups/{group_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
{
"features": [
{
"geometry": {
"felt:id": "luCHyMruTQ6ozGk3gPJfEB",
"felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
},
"properties": {},
"type": "Feature"
}
],
"type": "FeatureCollection"
}
The ID of the map to list groups from.
GET /api/v2/maps/{map_id}/element_groups HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
[
{
"color": "text",
"elements": {
"features": [
{
"geometry": {
"felt:id": "luCHyMruTQ6ozGk3gPJfEB",
"felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
},
"properties": {},
"type": "Feature"
}
],
"type": "FeatureCollection"
},
"id": "text",
"name": "text",
"symbol": "text"
}
]
The ID of the map to create the element in
POST /api/v2/maps/{map_id}/elements HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
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"
}
The ID of the map to create the group in
#C93535
luCHyMruTQ6ozGk3gPJfEB
My Element Group
dot
POST /api/v2/maps/{map_id}/element_groups HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
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"
}
]
The ID of the map to delete the element from.
The ID of the element to delete.
DELETE /api/v2/maps/{map_id}/elements/{element_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
No content
GET /api/v2/user HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
{
"email": "text",
"id": "luCHyMruTQ6ozGk3gPJfEB",
"name": "text"
}
The ID of the map to export comments from.
The format to export the comments in, either 'csv' or 'json' (default)
GET /api/v2/maps/{map_id}/comments/export HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
[]
The ID of the map that contains the comment.
The ID of the comment to resolve.
POST /api/v2/maps/{map_id}/comments/{comment_id}/resolve HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
{
"comment_id": "luCHyMruTQ6ozGk3gPJfEB"
}
The ID of the map that contains the comment.
The ID of the comment to delete.
DELETE /api/v2/maps/{map_id}/comments/{comment_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
No content
Only needed when using the API as part of a plugin
GET /api/v2/sources HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
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"
}
]
The ID of the source to show
GET /api/v2/sources/{source_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
{
"automatic_sync": "enabled",
"connection": {
"account_id": "text",
"database": "text",
"role": "text",
"schema": "text",
"type": "snowflake",
"user": "text",
"warehouse": "text"
},
"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"
}
POST /api/v2/maps/{map_id}/add_source_layer HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
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"
}
POST /api/v2/sources HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Accept: */*
Content-Length: 211
{
"connection": {
"account_id": "text",
"database": "text",
"password": "text",
"role": "text",
"schema": "text",
"type": "snowflake",
"user": "text",
"warehouse": "text"
},
"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"
}
The ID of the source to sync
POST /api/v2/sources/{source_id}/sync HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
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"
}
The ID of the source to update
POST /api/v2/sources/{source_id}/update HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Accept: */*
Content-Length: 211
{
"connection": {
"account_id": "text",
"database": "text",
"password": "text",
"role": "text",
"schema": "text",
"type": "snowflake",
"user": "text",
"warehouse": "text"
},
"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"
}
The ID of the source to delete
DELETE /api/v2/sources/{source_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
No content
The ID of the map to upload the layer to.
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.
The display name for the new layer.
(Image uploads only) The zoom level of the image.
POST /api/v2/maps/{map_id}/upload HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
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"
}
The ID of the map hosting the layer to refresh
The ID of the layer to refresh
POST /api/v2/maps/{map_id}/layers/{layer_id}/refresh HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
{
"layer_group_id": "luCHyMruTQ6ozGk3gPJfEB",
"layer_id": "luCHyMruTQ6ozGk3gPJfEB",
"presigned_attributes": {},
"type": "upload_response",
"url": "text"
}
The ID of the map to delete the layer from
The ID of the layer to delete
DELETE /api/v2/maps/{map_id}/layers/{layer_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
No content
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
POST /api/v2/maps/{map_id}/layers/{layer_id}/update_style HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Accept: */*
Content-Length: 12
{
"style": {}
}
{
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"links": {
"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",
"ordering_key": 1,
"progress": 1,
"refresh_period": "15 min",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
GET /api/v2/maps/{map_id}/layers HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
[
{
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"links": {
"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",
"ordering_key": 1,
"progress": 1,
"refresh_period": "15 min",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
]
GET /api/v2/maps/{map_id}/layers/{layer_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
{
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"links": {
"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",
"ordering_key": 1,
"progress": 1,
"refresh_period": "15 min",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
A very interesting dataset
luCHyMruTQ6ozGk3gPJfEB
luCHyMruTQ6ozGk3gPJfEB
My Layer
Deprecated: use caption
instead.
POST /api/v2/maps/{map_id}/layers HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Accept: */*
Content-Length: 373
[
{
"caption": "A very interesting dataset",
"id": "luCHyMruTQ6ozGk3gPJfEB",
"layer_group_id": "luCHyMruTQ6ozGk3gPJfEB",
"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"
}
]
[
{
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"links": {
"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",
"ordering_key": 1,
"progress": 1,
"refresh_period": "15 min",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
]
GET /api/v2/maps/{map_id}/layer_groups/{layer_group_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
{
"caption": "text",
"id": "luCHyMruTQ6ozGk3gPJfEB",
"layers": [
{
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"links": {
"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",
"ordering_key": 1,
"progress": 1,
"refresh_period": "15 min",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
],
"links": {
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6D"
},
"name": "text",
"ordering_key": 1,
"type": "layer_group",
"visibility_interaction": "default"
}
The ID of the map to delete the layer group from
The ID of the layer group to delete
DELETE /api/v2/maps/{map_id}/layer_groups/{layer_group_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
No content
A very interesting group
luCHyMruTQ6ozGk3gPJfEB
My Layer Group
Deprecated: use caption
instead.
Controls how the layer group is displayed in the legend. Defaults to "default"
.
POST /api/v2/maps/{map_id}/layer_groups HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Accept: */*
Content-Length: 146
[
{
"caption": "A very interesting group",
"id": "luCHyMruTQ6ozGk3gPJfEB",
"name": "My Layer Group",
"ordering_key": 1,
"visibility_interaction": "default"
}
]
[
{
"caption": "text",
"id": "luCHyMruTQ6ozGk3gPJfEB",
"layers": [
{
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"links": {
"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",
"ordering_key": 1,
"progress": 1,
"refresh_period": "15 min",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
],
"links": {
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6D"
},
"name": "text",
"ordering_key": 1,
"type": "layer_group",
"visibility_interaction": "default"
}
]
A very interesting group
luCHyMruTQ6ozGk3gPJfEB
My Layer Group
Deprecated: use caption
instead.
Controls how the layer group is displayed in the legend. Defaults to "default"
.
POST /api/v2/maps/{map_id}/layer_groups/{layer_group_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Accept: */*
Content-Length: 144
{
"caption": "A very interesting group",
"id": "luCHyMruTQ6ozGk3gPJfEB",
"name": "My Layer Group",
"ordering_key": 1,
"visibility_interaction": "default"
}
{
"caption": "text",
"id": "luCHyMruTQ6ozGk3gPJfEB",
"layers": [
{
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"links": {
"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",
"ordering_key": 1,
"progress": 1,
"refresh_period": "15 min",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
],
"links": {
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6D"
},
"name": "text",
"ordering_key": 1,
"type": "layer_group",
"visibility_interaction": "default"
}
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.
workspace
Possible values: GET /api/v2/library HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
{
"layer_groups": [
{
"caption": "text",
"id": "luCHyMruTQ6ozGk3gPJfEB",
"layers": [
{
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"links": {
"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",
"ordering_key": 1,
"progress": 1,
"refresh_period": "15 min",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
],
"links": {
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6D"
},
"name": "text",
"ordering_key": 1,
"type": "layer_group",
"visibility_interaction": "default"
}
],
"layers": [
{
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"links": {
"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",
"ordering_key": 1,
"progress": 1,
"refresh_period": "15 min",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
],
"type": "layer_library"
}
The ID of the map where the layer is located
The ID of the layer to publish
The name to publish the layer under
My Layer
POST /api/v2/maps/{map_id}/layers/{layer_id}/publish HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Accept: */*
Content-Length: 19
{
"name": "My Layer"
}
{
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"links": {
"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",
"ordering_key": 1,
"progress": 1,
"refresh_period": "15 min",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
The ID of the map where the layer group is located
The ID of the layer group to publish
The name to publish the layer group under
My Layer
POST /api/v2/maps/{map_id}/layer_groups/{layer_group_id}/publish HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Accept: */*
Content-Length: 19
{
"name": "My Layer"
}
{
"caption": "text",
"id": "luCHyMruTQ6ozGk3gPJfEB",
"layers": [
{
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"links": {
"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",
"ordering_key": 1,
"progress": 1,
"refresh_period": "15 min",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
],
"links": {
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6D"
},
"name": "text",
"ordering_key": 1,
"type": "layer_group",
"visibility_interaction": "default"
}
The ID of the map where the layer is located
The ID of the layer to export
GET /api/v2/maps/{map_id}/layers/{layer_id}/get_export_link HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
{
"export_link": "text"
}
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
csv
Possible values: POST /api/v2/maps/{map_id}/layers/{layer_id}/custom_export HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
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"
}
The ID of the map where the layer is located
The ID of the layer to export
The ID of the export
GET /api/v2/maps/{map_id}/layers/{layer_id}/custom_exports/{export_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
{
"download_url": "https://us1.data-pipeline.felt.com/fcdfd96c-06fa-40b9-9ae9-ad034b5a66df/Felt-Export.zip",
"export_id": "FZWQjWZJSZWvW3yn9BeV9AyA",
"filters": [],
"status": "completed"
}
POST /api/v2/duplicate_layers HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Accept: */*
Content-Length: 92
[
{
"destination_map_id": "luCHyMruTQ6ozGk3gPJfEB",
"source_layer_id": "luCHyMruTQ6ozGk3gPJfEB"
}
]
{
"layer_groups": [
{
"caption": "text",
"id": "luCHyMruTQ6ozGk3gPJfEB",
"layers": [
{
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"links": {
"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",
"ordering_key": 1,
"progress": 1,
"refresh_period": "15 min",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
],
"links": {
"self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6D"
},
"name": "text",
"ordering_key": 1,
"type": "layer_group",
"visibility_interaction": "default"
}
],
"layers": [
{
"caption": "text",
"geometry_type": "Line",
"hide_from_legend": true,
"id": "luCHyMruTQ6ozGk3gPJfEB",
"is_spreadsheet": true,
"links": {
"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",
"ordering_key": 1,
"progress": 1,
"refresh_period": "15 min",
"status": "uploading",
"style": {},
"tile_url": "text",
"type": "layer"
}
]
}