Only this pageAll pages
Powered by GitBook
1 of 60

Home

Loading...

REST API

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

JS SDK

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Felt Style Language

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

API Reference

Types of visualizations

Maps

APIs for building maps

Maps are the centerpiece of Felt.

With these APIs, you can create, retrieve, update, delete, move, and duplicate maps programmatically.

Layer Library

APIs to publish layers

With these APIs, you can publish your layers to your workspace library.

Elements

APIs for drawing spatially

Elements enable you to annotate maps with custom shapes, text, and markers.

With these APIs, you can create, update, and delete map elements.

Overview

Felt’s Developer Tools

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.

Creating and modifying maps

Felt’s 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 module, which can be installed with pip and used to call the REST API endpoints directly from Python functions.

Creating custom applications

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 allows developers to programmatically control maps in two ways: let you write custom code directly within Felt maps, with access to all SDK functionality. Alternatively, you can Felt maps into your own applications and use the SDK to control the embedded map experience.

Layer Uploads

APIs to upload data

With these APIs, you can upload your data to create new layers.

Layer Exports

APIs to export layer data

With these APIs, you can export data to CSV, GeoJSON, and other formats.

Sources

APIs to connect your data

Sources connect your databases to Felt.

With these APIs, you can configure data source connections, credentials, and sync settings to create live maps.

Comments

APIs for programatic collaboration

Comments bring conversations to mapping.

With these APIs, you can export, resolve, and delete map comments and collaboration threads.

Embed Tokens

APIs to share maps securely

Embed tokens enable safely sharing your private maps.

With these APIs, you can generate secure tokens for embedding maps.

Users

APIs for user information

Users represent the people in your workspace.

With these APIs, you can retrieve user profile information.

Layers

APIs to visualize spatial data

Layers enable you to visualize, style and interact with your spatial data.

With these APIs, you can upload data, manage layer styling, publish and refresh live data layers.

Getting started

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.

Style definition blocks

Learn how to define and configure the code blocks that compose the Felt Style Language

Types of visualizations

Learn about visualization types, including simple, categorical, numeric (color by & size by), heatmaps and hillshade.

Legends

Details on how to customize legends on a per-layer basis.

Errors

Definitions for errors raised when validating the Felt Style Language.

REST API
felt-python
JavaScript SDK
Extensions
embed

Navigating maps and workspaces

Workspaces and API tokens

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.

Working with maps

Creating a new map

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.

Getting a map's details

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)

Deleting a map

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)

Moving a map

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)
Style definition blocks
Types of visualizations
Legends
Errors

Getting started

The Felt SDK allows you to control your Felt maps and build powerful, interactive custom applications. You can control many aspects of the Felt UI and map contents, as well as receive notifications of events happening in the map such as clicks, selections, and more.

This feature is available to customers on the Enterprise plan. All new accounts automatically include a 7-day trial of Enterprise plan features.

See our examples page to explore what you can build with the SDK.

There are two main ways to use the Felt SDK:

  1. Extensions

  2. Embedded Maps

Extensions

Write code directly within Felt using our Extensions feature. Extensions run directly within the Felt environment, giving you immediate access to all SDK functionality without embedding or connection steps.

When creating an extension, you automatically have access to a FeltController object with no setup required. This controller provides all the methods you need to interact with your Felt map, including getViewport, createElement, setLayerStyle, and many more.

// In a Felt extension, the controller is automatically available
const layers = await felt.getLayers();
const elements = await felt.getElements();

// Listen for map events
felt.onSelectionChange((selection) => {
  console.log('Selection changed:', selection);
});

Embedded Maps

Embed Felt maps in your own applications and control them remotely. This approach requires connecting to a map embed.

Installation

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.

React Integration

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.

Projects

APIs to organize maps

Projects help you organize maps and manage team permissions.

With these APIs, you can manage the projects in your workspace.

Reading entities

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.

Getting entities

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:

Using constraints

All of the methods that return multiple entities accept constraint parameters to filter the results:

Reacting to changes

To stay in sync with entities, use the appropriate on[EntityType]Change method. For example, to monitor layer changes:

Best practices

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

  2. Error handling: When getting entities, remember that the methods may return null if the entity doesn't exist:

  1. Batch operations: When you need multiple entities, use the bulk methods (getLayers, getElements, etc.) with constraints rather than making multiple individual calls:

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.

Style definition blocks

The shape of a style definition

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.

Field name
Description

The filters block

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

H3

H3 visualization is a way to aggregate point data into a grid of H3 cells.

H3 visualizations are defined using “type”: “h3” . They generally share the same properties and behaviors as . Properties of specific relevance to H3 are:

Field name
Description

This is an example of an H3 visualization

defined with the following style

Zoom-based Styling

Zoom-based styling is useful to change how features and labels are shown at different zoom levels.

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.

// Get a single layer
const layer = await felt.getLayer("layer-1");

// Get all layers
const layers = await felt.getLayers();
// 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"],
});
felt.onLayerChange({
  options: {id: "layer-1"},
  handler: ({layer}) => {
    console.log(layer.visible);
  }
});
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");
}
// 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");

version

Mandatory. Defines which version this style adheres to

type

Optional. One of simple , categorical , numeric or heatmap. Defaults to simple.

config

Optional. A block that contains some configuration options to be used across the style. Learn more.

style

Optional. An object that defines how the data will be drawn. Learn more.

label

Optional. An object that defines how the labels will be drawn. Learn more.

legend

Optional. Defines how this layer will be shown on the layer panel. Learn more.

popup

Optional. Defines how the popup is shown and what’s included. Learn more.

attributes

Optional. Defines how attributes are shown both in the popup and the table. Learn more.

filters

Optional. A data filter definition that defines which data will be rendered. Learn more.

{
  "version": "2.1",
  "type": "simple",
  "style": {...}
}
Example of a filter block that filters out features with a value less than 50000 on the “acres” property
"filters": ["acres", "lt", 50000]
Example of a more complex filter block
"filters": [["acres", "ge", 50000], "and", ["acres", "le", 70000]]

aggregation

The aggregation method that will be used on points within each cell. Supported values are count (default), sum, min, max, and mean .

binMode

fixed (default), low, medium, or high. Determines if the resolution of H3 cell is fixed or automatically determined based on the current zoom of the map.

baseBinLevel

Required. If binMode is fixed, this is the H3 cell resolution that the map will use. H3 cells vary from resolution 1 (largest) to resolution 15 (smallest). If binMode is auto, this is the resolution that will be used for calculating class breaks. You will get best results choosing a resolution that matches the fixed resolution you would choose to fit the most commonly-viewed zoom for your map.

numericAttribute

The numeric column to aggregate. Required unless aggregation is count , in which case the column choice is irrelevant.

{
  "config": {
    "steps": {"type": "quantiles", "count": 5},
    "aggregation": "sum",
    "binMode": "fixed",
    "baseBinLevel": 3,
    "numericAttribute": "capacity_mw"
  },
  "paint": {"color": "@riverine"},
  "type": "h3",
  "version": "2.3.1",
  "label": {},
  "legend": {"displayName": "auto"}
}
color range visualizations for polygons
"paint": {
  "color": {"linear": [[14, "red"], [20, "blue"]]},
  ...
}
the full specification
Interpolators
Features at zoom level 14
Features at zoom level 17
Features at zoom level 20

Styling layers

Understanding layer styles

A layer's style is defined in a JSON-based called the Felt Style Language, 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:

{
  "config": {"labelAttribute": ["type"]},
  "legend": {},
  "paint": {
    "color": "blue",
    "opacity": 0.9,
    "size": 30,
    "strokeColor": "auto",
    "strokeWidth": 1
  },
  "type": "simple",
  "version": "2.1"
}

Fetching a layer's current style

A layer's FSL can be retrieved by performing a simple GET request to a layer's endpoint:

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

Updating an existing layer's style

To update a layer's style, we can send a POST request with the new FSL to the same layer's /update_style endpoint.

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

FSL examples

You can find examples of FSL for different visualization types in the Felt Style Language section of these docs:

  • Simple visualizations: same color and size for all features (vector) or pixels (raster).

  • Categorical visualizations: different color per feature or pixel, based on a categorical attribute

  • Numeric visualizations: different color or size per feature or pixel, based on a numeric attribute.

  • Heatmaps: a density-based visualization style, for vector point layers.

  • Hillshade: a special kind of visualization for raster elevation layers.

Hiding and showing

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.

Understanding visibility requests

All visibility methods use a consistent structure that allows both showing and hiding entities in a single call:

{
  show?: string[],  // IDs of entities to show
  hide?: string[]   // IDs of entities to hide
}

Layers

Control visibility of layer groups using setLayerVisibility:

felt.setLayerVisibility({
  show: ["layer-1", "layer-2"],
  hide: ["layer-3"]
});

Layer groups

Control visibility of layer groups using setLayerGroupVisibility:

felt.setLayerGroupVisibility({
  show: ["group-1", "group-2"],
  hide: ["group-3"]
});

Element groups

Similarly, control element group visibility with setElementGroupVisibility:

felt.setElementGroupVisibility({
  show: ["points-group"],
  hide: ["lines-group", "polygons-group"]
});

Legend items

Legend items require both a layer ID and an item ID to identify them. Use setLegendItemVisibility:

felt.setLegendItemVisibility({
  show: [
    { layerId: "layer-1", id: "item-1" },
    { layerId: "layer-1", id: "item-2" }
  ],
  hide: [
    { layerId: "layer-1", id: "item-3" }
  ]
});

Common use cases

Focusing on a single layer

To focus on a single layer by hiding all others, first get all layers and then use their IDs:

const layers = await felt.getLayers();
const targetLayerId = "important-layer";

felt.setLayerVisibility({
  show: [targetLayerId],
  hide: layers
    .map(layer => layer?.id)
    .filter(id => id && id !== targetLayerId)
});

Toggling visibility

When implementing a toggle, you can use empty arrays for the operation you don't need:

function toggleLayer(layerId: string, visible: boolean) {
  felt.setLayerVisibility({
    show: visible ? [layerId] : [],
    hide: visible ? [] : [layerId]
  });
}

Best practices

  1. Batch operations: Use a single call with multiple IDs rather than making multiple calls:

// 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"] });
  1. Omit unused properties: When you only need to show or hide, omit the unused property rather than including it with an empty array:

// Do this
felt.setLayerVisibility({
  show: ["layer-1"]
});

The popup block

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:

Field name
Description

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.

Example of a popup block
"popup": {
  "keyAttributes": [
    "osm_id",
    "barriers",
    "highway",
    "ref",
    "is_in",
    "place",
    "man_made",
    "other_tags"
  ],
  "titleAttribute": "barriers",
  "popupLayout": "list"
}

Controlling maps

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

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.

Using Felt.embed to create an iframe

Create 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%";

Using Felt.embed to mount into an existing iframe

In some cases, you may want to add a "template" iframe to your page. This can be useful if you want to style your iframe in a specific way, or if you already have one map embedded and want to mount and control a different map.

In this case, you can call Felt.embed with the iframe element as the first argument:

<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",
);

Using Felt.connect to connect to an existing embedded Felt map

There may be cases where you already have a Felt map embedded in an iframe, and you want to control it using the SDK. This can be useful if your HTML is server-rendered with the Felt map already embedded.

In this case, you can call Felt.connect with the iframe's window as the first argument:

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

Examples

Explore examples of what you can build with the Felt SDK. These examples showcase different approaches to creating interactive map experiences - from extensions that run directly within Felt to embedded maps in custom applications.

Extensions

Extensions run directly within Felt maps, giving you immediate access to all SDK functionality. Here are some examples built using AI assistance:

Commuter patterns

Visualize transportation patterns by drawing lines to destination counties on click. Features travel mode options and filtering capabilities to analyze commuting data across different regions. View the map here.

Neighborhood comparison

Compare neighborhoods side-by-side with automated analysis of land use patterns. This tool helps users understand demographic and geographic differences between areas. View the map here.

Animated data

Bring geographic data to life with animations showing the flow of the Mississippi River Basin from headwaters to the Gulf of Mexico, demonstrating how to create compelling temporal visualizations. View the map here.

Story map

Guide users through agricultural regions with an interactive narrative experience that combines storytelling with geographic exploration. View the map here.

Embedded maps

Embed Felt maps in your own applications and control them with the SDK. Here are some examples built with React and hosted on CodeSandbox:

Rooftop Solar Potential

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

Sales Dashboard

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

Custom legend with nested folders

Enhance map usability with a custom legend that extracts and uses FSL 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 here.

Inset maps

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

Lens Map

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

Isochrones

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

Heatmaps

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:

Field name
Description

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.

Working with selection

The Felt SDK provides functionality for reading the current selection state and selecting features on the map programmatically. This is useful for building interactive experiences that respond to data analysis or user interactions.

Selecting Features

Features are individual data points within layers. You can select features programmatically using the method. Only one feature can be selected at a time - selecting a new feature will replace the current selection.

The method accepts options to control the selection behavior:

  • showPopup (boolean, default: true) - Whether to display the feature information popup

  • fitViewport (boolean | { maxZoom: number }, default: true) - Whether to fit the viewport to the feature. Can be true, false, or an object with maxZoom to limit the zoom level used.

Reading selection

You can get the current selection state using . This returns an array of objects, each representing a selected entity. The selection can include various types of entities at the same time, such as elements and features:

Clearing Selection

Remove current selections using :

Reacting to selection changes

To stay in sync with selection changes, use the method:

Best practices

  1. Clean up listeners: Always store and call the unsubscribe function when you no longer need to listen for selection changes:

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

Listening to updates using webhooks

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.

Requirements

Two things are needed in order to make use of webhooks:

  1. A Felt map which will serve as the basis for the webhook. Updates will be sent whenever something on this map changes.

  2. A webhook URL where the updates will be sent in the form of POST requests.

Generating a new webhook

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.

Using your new webhook

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.

Setting up an example webhook using Pipedream

is an easy way to collect webhook requests and even run custom code as a result.

  1. Create a free Pipedream account

  2. On the left-hand sidebar, navigate to Sources, then click on New source in the top-right corner.

  3. Select HTTP / Webhook, then New Requests (Payload Only), and give your newly-created source a name.

  4. Copy the endpoint URL. It will look like https://XXX.m.pipedream.net

  5. In Felt, navigate to the and click on Create new webhook

  6. Paste the endpoint URL from Pipedream into the Webhook URL text field, select your map in the dropdown and click Create.

To test your webhook:

  1. Navigate to the Felt map that's linked to the webhook

  2. Make any change: add a pin, draw with the marker, change the color of a polygon, update sharing permissions...

  3. Back in Pipedream, verify that new events appear for the new source. The payload should look like this:

  1. You may also add configure a script to run using the above input.

Setting up an example webhook using an AWS Lambda

Serverless functions like AWS Lambda or Google Cloud Functions are an excellent way of triggering code after a map update by setting them to run after a specific HTTP call.

  1. In the AWS console, navigate to Lambda and click on Create function

  2. Choose a name and runtime and, under Advanced Settings, make sure to check Enable function URL

  3. Set Auth type to NONE and click on Create function

  4. In the next screen, copy the Function URL. It should look like https://{LAMBDA_ID}.lambda-url.{REGION}.on.aws

  5. Continue configuring your Lambda function as usual by editing the code that will run on map updates

In Felt:

  1. Navigate to the and click on Create new webhook

  2. Paste the function URL from AWS into the Webhook URL text field, select your map in the dropdown and click Create.

Layer filters

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.

Understanding filter sources

Layer filters can come from multiple sources, which are combined to create the final filter:

  1. Style filters: Set by the map creator in the Felt UI

  2. Component filters: Set through interactive legend components

  3. Ephemeral filters: Set temporarily through the SDK

You can inspect these different filter sources using getLayerFilters:

Setting filters

Use setLayerFilters to apply ephemeral filters to a layer:

Filter operators

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 for more details on filter operators.

Compound filters

You can combine multiple conditions using boolean operators:

Practical example: Filtering by selected feature

Here's a common use case where we filter a layer to show only features that match a property of a selected feature:

Best practices

  1. Clear filters: Set filters to null to remove them entirely:

  1. Check existing filters: Remember that your ephemeral filters combine with existing style and component filters:

  1. Type safety: Use TypeScript to ensure your filter expressions are valid:

General concepts

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.

Use of promises

All methods in the Felt SDK are asynchronous and return Promises. This means you'll need to use await or .then() when calling them:

Getting entities

The SDK follows a consistent pattern for getting entities. For each entity type, there are usually two methods:

  1. A singular getter for retrieving one entity by ID:

  1. A plural getter that accepts constraints for retrieving multiple entities:

  1. The plural getters allow you to pass no constraints, in which case they'll return all entities of that type:

Change listeners

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:

Cleanup functions

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:

Handler and options structure

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:

Entity nodes

When dealing with mixed collections of entities (like in selection events), each entity is wrapped in an EntityNode object that includes type information:

Map interactions and viewport

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.

Working with the viewport

Getting viewport state

You can get the current viewport state using getViewport():

Setting the viewport

There are two main ways to set the viewport: moving to a specific point, or fitting to bounds.

Moving to a point

Use setViewport() to move the map to a specific location:

Fitting to bounds

Use fitViewportToBounds() to adjust the viewport to show a specific rectangular area:

Responding to viewport changes

To stay in sync with viewport changes, use the onViewportMove method:

Map interactions

Click events

Listen for click events on the map using onPointerClick:

Hover events

Track mouse movement over the map using onPointerMove:

Best practices

  1. Cleanup: Always store and call unsubscribe functions when you're done listening for events:

  1. Throttling: For pointer move events, consider throttling your handler if you're doing expensive operations:

By using these viewport controls and interaction handlers, you can create rich, interactive experiences with your Felt map.

Integrating with React

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.

Embedding with useFeltEmbed

useFeltEmbed implementation

Getting live data

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

Refreshing live data 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

Refreshing a layer with a file

Refreshing a file is a single function call using the felt-python library.

Just like , refreshing a layer with a new file is a two-step process:

1. Request a refresh via the Felt API

Perform a POST request to receive an S3 presigned URL which you can later upload your files to:

2. Upload your file(s) to Amazon s3

Refreshing a layer with a URL

Similar to , refreshing an existing URL layer is just a matter of making a single POST request:

Hillshade

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:

Field name
Description

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

Authentication

All calls to the Felt API require authorization using a Bearer token in the request header:

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.

API tokens are scoped to a workspace, meaning that you can only work with resources associated to that workspace only.

You can create an API token in the :

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:

Here's an example showing how to create a new Felt map:

Simple visualizations

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.

Vector example

The Airports layer in Felt is an example of a simple visualization using a vector dataset

and is defined by the following style:

Raster example

This is an example of a simple visualization using a raster dataset

and is defined by the following style:

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);
      }
    });
  }
});
const viewport = await felt.getViewport();
console.log(viewport.center); // { latitude: number, longitude: number }
console.log(viewport.zoom);   // number
felt.setViewport({
  center: {
    latitude: 37.7749,
    longitude: -122.4194
  },
  zoom: 12
});
felt.fitViewportToBounds({
  bounds: [
    west,   // minimum longitude
    south,  // minimum latitude
    east,   // maximum longitude
    north   // maximum latitude
  ]
});
const unsubscribe = felt.onViewportMove({
  handler: (viewport) => {
    console.log("New center:", viewport.center);
    console.log("New zoom:", viewport.zoom);
  }
});

// Clean up when done
unsubscribe();
const unsubscribe = felt.onPointerClick({
  handler: (event) => {
    // Location of the click
    console.log("Click location:", event.center);
    
    // Features under the click
    console.log("Clicked features:", event.features);
    
    // The pixel coordinates of the cursor, measured from the top left corner of the map DOM element.
    console.log("Screen coordinates:", event.point);
    
    // Raster values, if applicable
    const {value, categoryName, color} = event.rasterValues;
  }
});
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);
    
    // The pixel coordinates of the cursor, measured from the top left corner of the map DOM element.
    console.log("Screen coordinates:", event.point);

    // Raster values, if applicable
    const {value, categoryName, color} = event.rasterValues;
  }
});
const unsubscribe = felt.onPointerMove({
  handler: (event) => {
    // Handle event...
  }
});

// Later, when you're done:
unsubscribe();
import { throttle } from "lodash";

felt.onPointerMove({
  handler: throttle((event) => {
    // Handle frequent mouse moves...
  }, 100) // Limit to once every 100ms
});
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
felt.setLayerFilters({
  layerId: "layer-1",
  filters: ["POPULATION", "gt", 1000000]
});
felt.setLayerFilters({
  layerId: "layer-1",
  filters: [
    ["POPULATION", "gt", 1000000],
    "and",
    ["COUNTRY", "eq", "USA"]
  ]
});
// Listen for selection changes
felt.onSelectionChange({
  handler: async ({ selection }) => {
    // Find the first selected feature
    const selectedFeature = selection.find(node => node.type === "feature");
    
    if (selectedFeature) {
      // Get the state code from the selected feature
      const stateCode = selectedFeature.entity.properties.STATE_CODE;
      
      // Filter the counties layer to show only counties in the selected state
      felt.setLayerFilters({
        layerId: "counties-layer",
        filters: ["STATE_CODE", "eq", stateCode]
      });
    } else {
      // Clear the filter when nothing is selected
      felt.setLayerFilters({
        layerId: "counties-layer",
        filters: null
      });
    }
  }
});
felt.setLayerFilters({
  layerId: "layer-1",
  filters: null
});
const filters = await felt.getLayerFilters("layer-1");

// Check if there are any style filters before adding ephemeral ones
if (filters.style) {
  console.log("This layer already has style filters");
}
import type { Filters } from "@feltmaps/sdk";

const filter: Filters = ["POPULATION", "gt", 1000000];
felt.setLayerFilters({
  layerId: "layer-1",
  filters: filter
});
here
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;
}
felt/js-sdk-starter-react
{
  "body": {
    "attributes": {
      "type": "map:update",
      "updated_at": "2024-04-29T12:16:46",
      "map_id": "Jzjr8gMKSrCOxZ1OSMT49CB"
    }
  }
}
Developers tab of the Workspace Settings page
Pipedream's RequestBin
Webhooks tab of your workspace
Webhooks tab of your workspace

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
  }
}
Authorization: Bearer <API Token>
Authorization: Bearer felt_pat_07T+Jmpk...
# 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())
Developers tab of the Workspace Settings page
Generate as many API tokens as you need
Give your API token a unique name
Make sure to copy your token to a secure location
{
  "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"
}
{
  "version": "2.3",
  "type": "simple",
  "config": {},
  "paint": {"isSandwiched": false, "opacity": 0.93}
}
# 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)
regular file uploads
a URL upload
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
)
await felt.selectFeature({
  id: "feature-123",
  layerId: "buildings-layer"
});
await felt.selectFeature({
  id: "feature-123",
  layerId: "buildings-layer",
  showPopup: false,           // Whether to show the feature popup (default: true)
  fitViewport: { maxZoom: 15 } // Fit viewport to feature with zoom limit
});
const selection = await felt.getSelection();

console.log(`${selection.length} items selected`);

selection.forEach(node => {
  switch(node.type) {
    case 'feature':
      console.log('Selected feature:', node.entity.id, 'from layer:', node.entity.layerId);
      break;
    case 'element':
      console.log('Selected element:', node.entity.name || node.entity.type);
      break;
  }
});
// Clear all selections
await felt.clearSelection();

// Clear specific types of selections
await felt.clearSelection({ 
  features: true,   // Clear feature selections
  elements: false   // Keep element selections
});
const unsubscribe = felt.onSelectionChange({
  handler: ({ selection }) => {
    // selection is an array of EntityNode objects
    console.log("Selected entities:", selection);
    
    // Check what's selected
    selection.forEach(node => {
      console.log("Entity type:", node.type);
      console.log("Entity ID:", node.entity.id);
    });
  }
});

// Don't forget to clean up when you're done
unsubscribe();
const unsubscribe = felt.onSelectionChange({
  handler: ({ selection }) => {
    // Handle selection...
  }
});

// Later, when you're done:
unsubscribe();
const unsubscribe = felt.onSelectionChange({
  handler: ({ selection }) => {
    if (selection.length === 0) {
      console.log("Nothing is selected");
      return;
    }
    // Handle selection...
  }
});

Categorical visualizations

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.

Vector example

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)

Raster example

The Cropscape CDL layer in Felt is an example of a categorical layer on a raster dataset

Screenshot 2024-04-12 at 13.25.09.png

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

Legends

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 legend block section.

Simple legend

The Biodiversity Hotspots layer in Felt has a simple visualization with a legend defined as follows:

"legend": {}

Categorical legend

The Plant Hardiness Zones layer in Felt has a categorical visualization with a legend defined as follows:

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

Numeric legends

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.

Stepped

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

Continuous

"legend": {
  "displayName": {
    "0": "2.34M", 
    "1": "714.65K", 
    "2": "33K"
  }
}

Heatmap legends

Heatmap legends are defined as follows:

"legend": {
  "displayName": {
    "0": "Low", 
    "1": "High"
 }
}

Notice that the displayName mapping goes from 0 (left value) to 1 (right value)

Interpolators

Interpolators

Interpolators are functions that use the current zoom level to get you a value. The following interpolators are currently supported:

Step

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

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

Exponential

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

Cubic Bezier

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

Default values

Working with elements

Elements live at the top layer of a map, and are created directly inside the Felt app.

Combining elements with is a great way to create interactive data apps in Felt.

Listing all elements on a map

Elements live at the top layer of a map, and are created directly inside the Felt app.

Elements are returned as a .

Listing all element groups

Returns a list of GeoJSON Feature Collections, one for each element group.

Create or update new elements

Each element is represented by a feature in the POSTed 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.

Delete an element

Elements can be deleted by referencing them by ID

Working with layers

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.

Creating GeoJSON layers

Use the method to add GeoJSON layers to your map. This method accepts different source types depending on where your GeoJSON data comes from.

From a URL

To create a layer from a GeoJSON file at a remote URL:

From a local file

To create a layer from a GeoJSON file on the user's device:

From GeoJSON data

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:

Styling by geometry type

When creating GeoJSON layers, you can specify different styles for each geometry type that might be found in your data. The SDK will create separate layers for each geometry type:

Each style should be a valid (Felt Style Language) style. If you don't specify styles, Felt will apply default styles based on the geometry type.

Deleting layers

To remove a GeoJSON layer:

Note that this only works for layers created via the SDK's createLayersFromGeoJson method, not for layers added through the Felt UI.

Refreshing GeoJSON layers

For GeoJSON layers created from URLs, you can set automatic refreshing:

At Creation Time: By setting the refreshInterval parameter when creating the layer. The refreshInterval parameter is optional and specifies how frequently (in milliseconds) the layer should be automatically refreshed from the URL. Valid values range from 250ms to 5 minutes (300,000ms). If set to null or omitted, the layer won't refresh automatically.

Manual Refresh: Simply replace the source property of any layer you have created, using the method to update the source data.

Examples

This page contains examples of different kinds of visualizations expressed in the Felt Style Language

Minimal visualization

Point layer example

Polygon layer example

Line layer example

Color category

Numeric visualization

Heatmap visualization

H3 visualization

{
  "version": "2.3",
  "type": "simple",
  "config": {},
  "paint": {},
  "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_"] },
  "paint": {
    "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"]
  },
  "paint": {
    "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
  }
}
{
  "config": {
    "steps": {"type": "quantiles", "count": 5},
    "aggregation": "sum",
    "binMode": "fixed",
    "baseBinLevel": 3,
    "numericAttribute": "capacity_mw"
  },
  "paint": {"color": "@riverine"},
  "type": "h3",
  "version": "2.3.1",
  "label": {},
  "legend": {"displayName": "auto"}
}
Graph showing a Step interpolator function
Graph showing a Linear interpolator function
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)
webhooks
GeoJSON Feature Collection
# 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)

Building custom charts

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.

Data analysis methods

The SDK offers three complementary approaches to analyze your map data:

1. Aggregates: single statistics

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 }

2. Categories: group by values

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 }
]
*/

3. Histograms: group by numeric ranges

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 }
]
*/

Working with filters

You can apply filters in two powerful ways:

  1. At the top level - Affects both which data is included and how values are calculated

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

Advanced filtering examples

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 }

Interactive visualization example

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.

const layerResult = await felt.createLayersFromGeoJson({
  source: {
    type: "geoJsonUrl",
    url: "https://example.com/data/neighborhoods.geojson",
    // Optional: Auto-refresh every 30 seconds
    refreshInterval: 30000
  },
  name: "Neighborhoods",
  caption: "Neighborhood boundaries for the city", // Optional
  description: "This layer shows the official neighborhood boundaries" // Optional
});

if (layerResult) {
  console.log("Created layer group:", layerResult.layerGroup);
  console.log("Created layers:", layerResult.layers);
}
// Assuming you have a File object from a file input
const fileInput = document.getElementById('geojson-upload');
const file = fileInput.files[0];

const layerResult = await felt.createLayersFromGeoJson({
  source: {
    type: "geoJsonFile",
    file: file
  },
  name: "User Uploaded Data"
});

if (layerResult) {
  // Store the layer ID for later reference
  const layerId = layerResult.layers[0].id;
}
const geojsonData = {
  type: "FeatureCollection",
  features: [
    {
      type: "Feature",
      geometry: {
        type: "Point",
        coordinates: [-122.4194, 37.7749]
      },
      properties: {
        name: "San Francisco",
        population: 874961
      }
    },
    // Additional features...
  ]
};

const layerResult = await felt.createLayersFromGeoJson({
  source: {
    type: "geoJsonData",
    data: geojsonData
  },
  name: "Dynamic Points"
});
const layerResult = await felt.createLayersFromGeoJson({
  name: "Styled Features",
  source: {
    type: "geoJsonUrl",
    url: "https://example.com/data/mixed-features.geojson"
  },
  geometryStyles: {
    Point: {
      paint: { 
        color: "red", 
        size: 8 
      }
    },
    Line: {
      paint: { 
        color: "blue", 
        size: 4 
      },
      config: { 
        labelAttribute: ["name"] 
      },
      label: { 
        minZoom: 0 
      }
    },
    Polygon: {
      paint: { 
        color: "green", 
        strokeColor: "darkgreen",
        fillOpacity: 0.5
      }
    }
  }
});
await felt.deleteLayer("layer-1");
const layerResult = await felt.createLayersFromGeoJson({
  source: {
    type: "geoJsonUrl",
    url: "https://example.com/data/realtime-sensors.geojson",
    refreshInterval: 60000  // Refresh every minute
  },
  name: "Live Sensor Data"
});
FSL

Sample application

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 sample Felt SDK Application

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 attributes block

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:

Field name
Description

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.

Example of an attribute blocks block
"attributes": {
  "faa": {
    "displayName": "FAA Code",
    "format": {
      "mantissa": 0,
      "thousandSeparated": true
    }
  },
  "wikipedia": {"displayName": "Wikipedia"},
}

Getting started

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 . Reach out to .

Endpoints

All Felt API endpoints are hosted at the following base URL:

Create an API token

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 :

Learn more about API tokens here:

Install our Python library (optional)

The easiest way to interact with the Felt API is by using our felt-python SDK. You can install it with the following command:

Example: Creating a new map

Creating a new map is as simple as making a POST request to the maps endpoint.

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.

Example: Uploading a layer from a URL

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 . In this case, we'll import all the recent earthquakes from :

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:

Example: Styling a layer

Layer styles are defined in the , a JSON-based specification that allows customizing a layer's style, legend, label and popups.

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:

Go to your map to see how your new layer looks:

Example: Refreshing a live data layer

A layer must have finished uploading successfully before it can be refreshed

Similar to , refreshing an existing URL layer is just a matter of making a single POST request:

Now go to your map and see if any new earthquakes have occured!

The config block

The config block contains configuration options for a given visualization.

These are the fields that each config block can contain:

Field name
Description

Default Values

Name
Points
Polygons
Lines
Raster

Examples

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

-

-

Example of a categorical config block
"config": [
	{
    "labelAttribute": ["Wikipedia", "faa"],
		"categoricalAttribute": "faa",
		"categories": ["faa-code-1", "faa-code-2", "faa-code-3"],
		"showOther": true,
		"otherOrder": "above"
	}
]
Example of a vector numeric config block
"config": [
	{
    "labelAttribute": ["Wikipedia", "faa"],
		"numericAttribute": "percentage",
		"steps": [1, 25, 50, 75, 100]
	}
]
Example of a raster numeric config block
"config": {
  "band": 1,
  "method": {"NDVI": {"NIR": 1, "R": 2}},
  "steps": [-0.5, 0.5]
},
numbro
https://felt.com/api/v2
pip install felt-python
# 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"]
# 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"]
# 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,
)
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)
Enterprise plan
set up a trial
Developers tab of the Workspace Settings page
many kinds of file and URL imports
the USGS' live GeoJSON feed
Felt Style Language
a URL upload
You can generate as many API tokens as you need. Make sure to copy them to a secure location!
Since we imported a live data feed, the points on your layer may look different.
Since we imported a live data feed, the points on your layer may look different.
Authentication

The legend block

The legend block contains information on how the legend will be displayed for this visualization.

Take a look at the legends section to see how it works.

These are the fields that each legend block can contain:

Field name
Description

displayName

Optional. In categorical visualizations, a dictionary that maps from each category to what will be displayed on the legend. In or visualizations, a dictionary that maps from the index of each class to what will be displayed on the legend.

Example of a categorical legend
"legend": {
  "displayName": {
    "category-1": "Category 1",
    "category-2": "Category 2"
  }
}
Example of a numerical legend
"legend": {
  "displayName": {
    "0": "-0.5", 
    "1": "0.5"
  }
}

Errors

Unexpected value or type

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.

Categorical visualization not working

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 .

numeric
heatmap
here
here

Drawing elements

The Felt SDK provides two main approaches for creating elements on your maps:

  1. Interactive Drawing: Configure and activate drawing tools for users to create elements manually

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

Interactive Drawing with Tools

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.

Tool types

Tool name
Element Type
Description

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.

Example

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

Programmatic Element Creation

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.

Example


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

Retrieving Element geometry

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

Listening for changes

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

Listening for element creation

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

Sample application: sending elements drawn by users to your backend

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 (color & size)

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:

Method
Description

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

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.

Map link:

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.

Map link:

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

Any time there are fewer colors than values in a numeric style, they are interpolated in the (hcl color space).

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

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.

Map link:

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

Uploading files and URLs

Felt supports a myriad of formats, both as files and hosted URLs, up to a limit of 5GB. Check out the full list .

Uploading a URL

The easiest way of uploading data into a Felt map via the API is to import from a URL. Here's an example importing all the recent earthquakes from the USGS' live GeoJSON feed:

# Your API token and map ID should look like this:
# FELT_API_TOKEN="felt_pat_ABCDEFUDQPAGGNBmX40YNhkCRvvLI3f8/BCwD/g8"
# MAP_ID="CjU1CMJPTAGofjOK3ICf1D"
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"]
import os

from felt_python import upload_url

# Setting your API token as an env variable can save
# you from repeating it in every function call
os.environ["FELT_API_TOKEN"] = "<YOUR_API_TOKEN>"

map_id = "<YOUR_MAP_ID>"

url_upload = upload_url(
    map_id=map_id,
    layer_url="https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_hour.geojson",
    layer_name="USGS Earthquakes",
)
layer_id = url_upload["layer_id"]

Like maps, layers also have unique identifiers. Make sure to take note of them for subsequent calls, like styling a layer or removing it.

Uploading a file

Uploading a file is a single function call using the felt-python library.

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:

1. Request an upload via the Felt API

Perform a POST request to receive an S3 presigned URL which you can later upload your files to:

r = requests.post(
    f"https://felt.com/api/v2/maps/{map_id}/upload",
    headers={
        "Authorization": f"Bearer {api_token}",
        "Content-Type": "application/json",
    },
    json={"name": "My new layer"},
)
assert r.ok
layer_id = r.json()["layer_id"]

presigned_upload = r.json()
from felt_python import upload_file

file_name = "<YOUR_FILE_WITH_EXTENSION>" # Example: regions.geojson

upload_file(
  map_id=map_id,
  file_name="YOUR_FILE_WITH_EXTENSION",
  layer_name="My new layer",
)

2. Upload your file(s) to Amazon s3

# 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

Monitoring progress

You can check the upload status of a layer by querying it:

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

UI components

Action Triggers and Custom Panels

The Felt SDK enables you to extend Felt maps with custom UI components that integrate seamlessly with the native interface. These extensions allow you to add interactive controls and custom workflows directly within the map experience.

UI Extension Points

Felt provides two primary ways to add custom UI to your maps:

Action Triggers appear as buttons in the left sidebar and provide quick access to custom actions. Think of them as shortcuts that users can click to trigger specific functionality in your application.

Custom Panels appear in the right sidebar and offer a full canvas for complex UI. These panels can contain forms, controls, and interactive elements that work together to create sophisticated user experiences.

Action Triggers

Action triggers are simple button controls that execute custom functions when clicked. They're perfect for actions that don't require additional user input - like applying filters, running calculations, or enabling an interaction mode.

Custom Panels

Custom panels provide a structured way to build complex UI within Felt. Each panel consists of three main sections that serve different purposes:

Panel Structure

Header - Contains the panel title, and an optional close button.

Body - Houses the main interactive elements like forms, selectors, and content areas. This is where users spend most of their time interacting with your custom functionality.

Footer - Typically contains primary action buttons like "Save", "Cancel", or "Apply". This creates a consistent pattern users expect from dialog-style interfaces. The footer sticks to the bottom of the panel, with a divider separating it from the body.

Getting Started with Panels

Create a panel by first generating an ID, then specifying its contents. You can control where panels appear using the initialPlacement parameter. When onClickClose is specified, a close button will be rendered in the header.

Panel Elements

Custom panels support a variety of interactive and display elements that can be combined to create rich user experiences:

Text Elements

display formatted content and support full Markdown rendering, allowing you to include headings, lists, links, and formatting within your panels.

TextInput Elements

elements allow users to enter custom values like names, descriptions, or numeric parameters.

Control Elements

Control elements allow users to choose from predefined options:

Available control elements include , , , and . Each element supports similar properties:

Button Elements

trigger actions and come in different styles to communicate their importance and effect. Buttons can have different variants (filled, outlined, and transparent) and tints (primary, accent, danger and default):

Primary filled buttons highlight the most important action in a context. Use sparingly - typically one per panel section.

Button Rows

Group related buttons together to create clear action hierarchies:

automatically handle spacing and alignment, ensuring your panels look polished and consistent.

Grid Elements

helps organize elements within panels to create complex layouts. It uses a grid property that follows the same syntax as the CSS shorthand grid property, and includes verticalAlignment and horizontalDistribution properties for precise control over layout positioning.

iframe Elements

allow you to embed external content by providing a URL to charts, dashboards, or other web applications directly within your panels.

Divider Elements

Divider elements provide visual separation between sections of content in your panels.

Panel State Management

Creating and Updating Panels

A panel is identified by its ID, which must be created using . Custom IDs are not supported to prevent conflicts with other panels. Use for most panel scenarios. This declarative method lets you specify what the panel should contain, and it handles both creating new panels and updating existing ones with the same API call.

Targeted Panel Element Updates

Use for granular control when you want to modify individual elements. Elements need IDs to be targeted for updates. You can also use to add elements and to remove elements by their IDs.

The label block

The label block defines how feature labels are rendered.

These are the properties available to define label rendering. Anchors can be lines or points, polygons are not supported.

Type
Applies to
Description

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 .

Default values

Name
Points
Lines
Centroids

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"

default values
here
await felt.createActionTrigger({
  actionTrigger: {
    label: "Check solar potential",
    onTrigger: async () => {
      // Enable polygon tool to allow a user to select a region
      await felt.setTool("polygon");
      // ...
    },
  }
});
const panelId = await felt.createPanelId();
await felt.createOrUpdatePanel({
  panel: {
    id: panelId,
    title: "Add report",
    body: [
      { type: "Select", placeholder: "Choose a neighborhood", options: [...] },
      { type: "Select", placeholder: "Choose a severity", options: [...] },
      { type: "TextInput", placeholder: "Email", onBlur: storeEmail },
    ],
    footer: [
      {
        type: "ButtonRow",
        align: "end",
        items: [
          { type: "Button", label: "Back", variant: "transparent", tint: "default", onClick: handleBack },
          { type: "Button", label: "Done", variant: "filled", tint: "primary", onClick: handleDone }
        ]
      }
    ]
    onClickClose: async () => {
      // Clean up
      await felt.deletePanel(panelId);
    }
  },
  initialPlacement: { at: "start" } // Optional: control panel positioning
});
{
  type: "Text",
  content: "**Welcome!** This is a *formatted* text element with [links](https://felt.com).",
}
{
  type: "TextInput",
  placeholder: "First name",
  value: "",
  onChange: (args) => {
    console.log("New value:", args.value);
  },
}
// Select dropdown
{
  type: "Select", // or "CheckboxGroup" | "RadioGroup" | "ToggleGroup"
  label: "Year",
  options: [
    { value: "2025", label: "2025" },
    { value: "2024", label: "2024" },
  ],
  value: "",
  onChange: (args) => {
    console.log("Selected:", args.value);
  }
}
{
  type: "Button",
  label: "Submit",
  variant: "filled", // "transparent" | "outlined"
  tint: "primary", // "default" | "accent" | "danger"
  onClick: async () => {
    // Handle button click
  }
}
{
  type: "ButtonRow",
  align: "end",
  items: [
    { type: "Button", label: "Clear", variant: "transparent", tint: "default", onClick: handleClear },
    { type: "Button", label: "Send report", variant: "filled", tint: "primary", onClick: handleSend }
  ]
}
{
  type: "Grid",
  grid: "auto-flow / 2fr 1fr", // CSS grid shorthand
  verticalAlignment: "start",
  horizontalDistribution: "stretch",
  items: [
    { type: "Text", content: "![image](https://example.com/image1.png)" },
    { type: "Text", content: "![image](https://example.com/image2.png) \n ![image](https://example.com/image3.png)" },
  ]
}
{
  type: "Iframe",
  url: "https://example.com/dashboard",
  height: 400,
}
{
  type: "Divider",
}
const panelId = await felt.createPanelId();
const greetingElement = { id: "greeting", type: "Text", content: "Hello" };

// Initial state
await felt.createOrUpdatePanel({
  panel: {
    id: panelId,
    title: "My Panel",
    body: [greetingElement]
  }
});

// Update using destructuring
await felt.createOrUpdatePanel({
  panel: {
    id: panelId,
    title: "My Panel",
    body: [{ ...greetingElement, content: "Hello World" }]
  }
});
const panelId = await felt.createPanelId();

// Create panel with multiple elements
await felt.createOrUpdatePanel({
  panel: {
    id: panelId,
    title: "Data Panel",
    body: [
      { id: "status-text", type: "Text", content: "Ready" },
      { 
        id: "layer-select", 
        type: "Select", 
        label: "Choose Layer",
        options: [
          { value: "layer1", label: "Population" },
          { value: "layer2", label: "Income" }
        ]
      }
    ]
  }
});

// Update only the text element
await felt.updatePanelElements({
  panelId,
  elements: [{
    element: {
      id: "status-text",
      type: "Text",
      content: "Processing data..."
    }
  }]
});

// Add a new element to the panel
await felt.createPanelElements({
  panelId,
  elements: [{
    element: { 
      id: "progress-text", 
      type: "Text", 
      content: "Progress: 50%" 
    },
    container: "body",
    placement: { at: "end" }
  }]
});

// Remove an element from the panel
await felt.deletePanelElements({
  panelId,
  elements: ["progress-text"]
});
Text elements
TextInput
Select
CheckboxGroup
RadioGroup
ToggleGroup
Button elements
Button rows
The grid element
iframe elements
createPanelId
createOrUpdatePanel
updatePanelElements
createPanelElements
deletePanelElements
Percent renter occupied housing by county
Average annual precipitation
Black Sea Grain Initiative
in our Help Center
setTool
ElementsController

The paint block

The paint block defines how feature geometries and raster pixels are rendered.

Properties common to all visualization types.

Type
Default
Description

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.

Simple visualizations

The following properties are available for the simple type of visualization

Type
Applies to
Description

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.

Categorical and numeric visualizations

The following properties are available for the categorical and numerical type visualizations. (You can see an example of a categorical visualization here)

Type
Applies to
Description

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

Label block reference

These are the properties available to define label rendering. Anchors can be lines or points, polygons are not supported.

Type
Applies to
Description

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.

Default values

Name
Points
Polygons
Lines

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

-

here
Interpolator
Interpolator
Interpolator
Interpolator
Interpolator
Interpolator
Interpolator
Interpolator
Interpolator
Interpolator
Interpolator
Interpolator
Interpolator
Interpolator
Interpolator
Interpolator
Interpolator
Interpolator
Interpolator
selectFeature()
selectFeature()
getSelection()
EntityNode
clearSelection()
onSelectionChange()
createLayersFromGeoJson
updateLayer
ToolsController
setToolSettings
onToolChange
onToolSettingsChange
getTool
getToolSettings
onElementCreate
onElementChange
createElement
updateElement
deleteElement
getElementGeometry
onElementCreate
onElementDelete
onElementChange
onElementCreateEnd
onElementCreateEnd
createElement
onElementCreate

Move map

post

Move a map to a different project or folder within the same workspace. Project IDs and Folder IDs can be found inside map settings.

Authorizations
Path parameters
map_idstringRequired
Body
one ofOptional
or
Responses
200

Map

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
post
/api/v2/maps/{map_id}/move
POST /api/v2/maps/{map_id}/move HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 39

{
  "project_id": "luCHyMruTQ6ozGk3gPJfEB"
}
{
  "basemap": "text",
  "created_at": "2024-05-25T15:51:34",
  "element_groups": [
    {
      "elements": {
        "features": [
          {
            "geometry": {
              "felt:id": "luCHyMruTQ6ozGk3gPJfEB",
              "felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
            },
            "properties": {},
            "type": "Feature"
          }
        ],
        "type": "FeatureCollection"
      },
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "name": "text"
    }
  ],
  "elements": {
    "features": [
      {
        "geometry": {
          "felt:id": "luCHyMruTQ6ozGk3gPJfEB",
          "felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
        },
        "properties": {},
        "type": "Feature"
      }
    ],
    "type": "FeatureCollection"
  },
  "folder_id": "text",
  "id": "luCHyMruTQ6ozGk3gPJfEB",
  "layer_groups": [
    {
      "caption": "text",
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "layers": [
        {
          "caption": "text",
          "geometry_type": "Line",
          "hide_from_legend": true,
          "id": "luCHyMruTQ6ozGk3gPJfEB",
          "is_spreadsheet": true,
          "legend_display": "default",
          "legend_visibility": "hide",
          "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"
        }
      ],
      "legend_visibility": "hide",
      "links": {
        "self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6D"
      },
      "name": "text",
      "ordering_key": 1,
      "type": "layer_group",
      "visibility_interaction": "default"
    }
  ],
  "layers": [
    {
      "caption": "text",
      "geometry_type": "Line",
      "hide_from_legend": true,
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "is_spreadsheet": true,
      "legend_display": "default",
      "legend_visibility": "hide",
      "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"
}

Duplicate map

post

Create a copy of a map with all its layers, elements, and configuration.

Authorizations
Path parameters
map_idstringRequired

The ID of the map to duplicate

Body
destinationone ofOptional
or
titlestringOptional

Title for the duplicated map. If not provided, will default to '[Original Title] (copy)'

Responses
200

Duplicated Map

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
post
/api/v2/maps/{map_id}/duplicate
POST /api/v2/maps/{map_id}/duplicate HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 70

{
  "destination": {
    "project_id": "luCHyMruTQ6ozGk3gPJfEB"
  },
  "title": "text"
}
{
  "basemap": "text",
  "created_at": "2024-05-25T15:51:34",
  "element_groups": [
    {
      "elements": {
        "features": [
          {
            "geometry": {
              "felt:id": "luCHyMruTQ6ozGk3gPJfEB",
              "felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
            },
            "properties": {},
            "type": "Feature"
          }
        ],
        "type": "FeatureCollection"
      },
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "name": "text"
    }
  ],
  "elements": {
    "features": [
      {
        "geometry": {
          "felt:id": "luCHyMruTQ6ozGk3gPJfEB",
          "felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
        },
        "properties": {},
        "type": "Feature"
      }
    ],
    "type": "FeatureCollection"
  },
  "folder_id": "text",
  "id": "luCHyMruTQ6ozGk3gPJfEB",
  "layer_groups": [
    {
      "caption": "text",
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "layers": [
        {
          "caption": "text",
          "geometry_type": "Line",
          "hide_from_legend": true,
          "id": "luCHyMruTQ6ozGk3gPJfEB",
          "is_spreadsheet": true,
          "legend_display": "default",
          "legend_visibility": "hide",
          "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"
        }
      ],
      "legend_visibility": "hide",
      "links": {
        "self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6D"
      },
      "name": "text",
      "ordering_key": 1,
      "type": "layer_group",
      "visibility_interaction": "default"
    }
  ],
  "layers": [
    {
      "caption": "text",
      "geometry_type": "Line",
      "hide_from_legend": true,
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "is_spreadsheet": true,
      "legend_display": "default",
      "legend_visibility": "hide",
      "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"
}

Create map

post

Create a new map with optional customization options.

Several aspects can be customized when creating a new map, including:

  • Title

  • Initial location (latitude, longitude and zoom level)

  • Sharing permissions (defaults to viewing and commenting for users with the map URL)

  • An array of URLs to import on map creation

Authorizations
Body
basemapstringOptional

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.

descriptionstringOptional

A description to display in the map legend

latnumberOptional

If no data has been uploaded to the map, the initial latitude to center the map display on.

layer_urlsstring[]Optional

An array of urls to use to create layers in the map. Only tile URLs for raster layers are supported at the moment.

lonnumberOptional

If no data has been uploaded to the map, the initial longitude to center the map display on.

public_accessstring · enumOptional

The level of access to grant to the map. Defaults to "view_only".

Possible values:
titlestringOptional

The title to be used for the map. Defaults to "Untitled Map"

workspace_idstringOptional

The workspace to create the map in. Defaults to the latest used workspace

zoomnumberOptional

If no data has been uploaded to the map, the initial zoom level for the map to display.

Responses
200

Map

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
post
/api/v2/maps
POST /api/v2/maps HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 149

{
  "basemap": "text",
  "description": "text",
  "lat": 1,
  "layer_urls": [
    "text"
  ],
  "lon": 1,
  "public_access": "private",
  "title": "text",
  "workspace_id": "text",
  "zoom": 1
}
{
  "basemap": "text",
  "created_at": "2024-05-25T15:51:34",
  "element_groups": [
    {
      "elements": {
        "features": [
          {
            "geometry": {
              "felt:id": "luCHyMruTQ6ozGk3gPJfEB",
              "felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
            },
            "properties": {},
            "type": "Feature"
          }
        ],
        "type": "FeatureCollection"
      },
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "name": "text"
    }
  ],
  "elements": {
    "features": [
      {
        "geometry": {
          "felt:id": "luCHyMruTQ6ozGk3gPJfEB",
          "felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
        },
        "properties": {},
        "type": "Feature"
      }
    ],
    "type": "FeatureCollection"
  },
  "folder_id": "text",
  "id": "luCHyMruTQ6ozGk3gPJfEB",
  "layer_groups": [
    {
      "caption": "text",
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "layers": [
        {
          "caption": "text",
          "geometry_type": "Line",
          "hide_from_legend": true,
          "id": "luCHyMruTQ6ozGk3gPJfEB",
          "is_spreadsheet": true,
          "legend_display": "default",
          "legend_visibility": "hide",
          "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"
        }
      ],
      "legend_visibility": "hide",
      "links": {
        "self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6D"
      },
      "name": "text",
      "ordering_key": 1,
      "type": "layer_group",
      "visibility_interaction": "default"
    }
  ],
  "layers": [
    {
      "caption": "text",
      "geometry_type": "Line",
      "hide_from_legend": true,
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "is_spreadsheet": true,
      "legend_display": "default",
      "legend_visibility": "hide",
      "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"
}

Update map

post

Update map properties including title, description, and access permissions.

Authorizations
Path parameters
map_idstringRequired

The ID of the map to update

Body
basemapstringOptional

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.

descriptionstringOptional

A description to display in the map legend

public_accessstring · enumOptional

The level of access to grant to the map. Defaults to "view_only".

Possible values:
titlestringOptional

The new title for the map

Responses
200

Map

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
post
/api/v2/maps/{map_id}/update
POST /api/v2/maps/{map_id}/update HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 278

{
  "basemap": "text",
  "description": "text",
  "public_access": "private",
  "table_settings": {
    "default_table_layer_id": "luCHyMruTQ6ozGk3gPJfEB",
    "viewers_can_open_table": true
  },
  "title": "text",
  "viewer_permissions": {
    "can_duplicate_map": true,
    "can_export_data": true,
    "can_see_map_presence": true
  }
}
{
  "basemap": "text",
  "created_at": "2024-05-25T15:51:34",
  "element_groups": [
    {
      "elements": {
        "features": [
          {
            "geometry": {
              "felt:id": "luCHyMruTQ6ozGk3gPJfEB",
              "felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
            },
            "properties": {},
            "type": "Feature"
          }
        ],
        "type": "FeatureCollection"
      },
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "name": "text"
    }
  ],
  "elements": {
    "features": [
      {
        "geometry": {
          "felt:id": "luCHyMruTQ6ozGk3gPJfEB",
          "felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
        },
        "properties": {},
        "type": "Feature"
      }
    ],
    "type": "FeatureCollection"
  },
  "folder_id": "text",
  "id": "luCHyMruTQ6ozGk3gPJfEB",
  "layer_groups": [
    {
      "caption": "text",
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "layers": [
        {
          "caption": "text",
          "geometry_type": "Line",
          "hide_from_legend": true,
          "id": "luCHyMruTQ6ozGk3gPJfEB",
          "is_spreadsheet": true,
          "legend_display": "default",
          "legend_visibility": "hide",
          "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"
        }
      ],
      "legend_visibility": "hide",
      "links": {
        "self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6D"
      },
      "name": "text",
      "ordering_key": 1,
      "type": "layer_group",
      "visibility_interaction": "default"
    }
  ],
  "layers": [
    {
      "caption": "text",
      "geometry_type": "Line",
      "hide_from_legend": true,
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "is_spreadsheet": true,
      "legend_display": "default",
      "legend_visibility": "hide",
      "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"
}

Get map

get

Retrieve a map with its metadata including title, URL, thumbnail, and timestamps.

Authorizations
Path parameters
map_idstringRequired
Responses
200

Map

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
get
/api/v2/maps/{map_id}
GET /api/v2/maps/{map_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
  "basemap": "text",
  "created_at": "2024-05-25T15:51:34",
  "element_groups": [
    {
      "elements": {
        "features": [
          {
            "geometry": {
              "felt:id": "luCHyMruTQ6ozGk3gPJfEB",
              "felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
            },
            "properties": {},
            "type": "Feature"
          }
        ],
        "type": "FeatureCollection"
      },
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "name": "text"
    }
  ],
  "elements": {
    "features": [
      {
        "geometry": {
          "felt:id": "luCHyMruTQ6ozGk3gPJfEB",
          "felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
        },
        "properties": {},
        "type": "Feature"
      }
    ],
    "type": "FeatureCollection"
  },
  "folder_id": "text",
  "id": "luCHyMruTQ6ozGk3gPJfEB",
  "layer_groups": [
    {
      "caption": "text",
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "layers": [
        {
          "caption": "text",
          "geometry_type": "Line",
          "hide_from_legend": true,
          "id": "luCHyMruTQ6ozGk3gPJfEB",
          "is_spreadsheet": true,
          "legend_display": "default",
          "legend_visibility": "hide",
          "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"
        }
      ],
      "legend_visibility": "hide",
      "links": {
        "self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6D"
      },
      "name": "text",
      "ordering_key": 1,
      "type": "layer_group",
      "visibility_interaction": "default"
    }
  ],
  "layers": [
    {
      "caption": "text",
      "geometry_type": "Line",
      "hide_from_legend": true,
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "is_spreadsheet": true,
      "legend_display": "default",
      "legend_visibility": "hide",
      "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"
}

Delete map

delete

Permanently delete a map and all its associated data.

This action cannot be undone. The map and all its layers, elements, and comments will be permanently removed.

Authorizations
Path parameters
map_idstringRequired

The ID of the map to delete

Responses
204

No Content

No content

401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
delete
/api/v2/maps/{map_id}
DELETE /api/v2/maps/{map_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*

No content

Publish map layer

post

Make a layer available in the workspace library for reuse by team members.

Authorizations
Path parameters
map_idstringRequired

The ID of the map where the layer is located

layer_idstringRequired

The ID of the layer to publish

Body
namestringOptional

The name to publish the layer under

Example: My Layer
Responses
200

Publish layer response

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
post
/api/v2/maps/{map_id}/layers/{layer_id}/publish
POST /api/v2/maps/{map_id}/layers/{layer_id}/publish HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 19

{
  "name": "My Layer"
}
{
  "caption": "text",
  "geometry_type": "Line",
  "hide_from_legend": true,
  "id": "luCHyMruTQ6ozGk3gPJfEB",
  "is_spreadsheet": true,
  "legend_display": "default",
  "legend_visibility": "hide",
  "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"
}

Publish map layer group

post

Make a layer group available in the workspace library for reuse by team members.

Authorizations
Path parameters
map_idstringRequired

The ID of the map where the layer group is located

layer_group_idstringRequired

The ID of the layer group to publish

Body
namestringOptional

The name to publish the layer group under

Example: My Layer
Responses
200

Publish layer group response

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
post
/api/v2/maps/{map_id}/layer_groups/{layer_group_id}/publish
POST /api/v2/maps/{map_id}/layer_groups/{layer_group_id}/publish HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 19

{
  "name": "My Layer"
}
{
  "caption": "text",
  "id": "luCHyMruTQ6ozGk3gPJfEB",
  "layers": [
    {
      "caption": "text",
      "geometry_type": "Line",
      "hide_from_legend": true,
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "is_spreadsheet": true,
      "legend_display": "default",
      "legend_visibility": "hide",
      "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"
    }
  ],
  "legend_visibility": "hide",
  "links": {
    "self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6D"
  },
  "name": "text",
  "ordering_key": 1,
  "type": "layer_group",
  "visibility_interaction": "default"
}

List library layers

get

List all layers in your workspace's library, or the felt layer library.

You can add a layer from the library to a map by using the "Duplicate layers" API endpoint (POST /api/v2/duplicate_layers) and the layer id provided by this endpoint.

Authorizations
Query parameters
sourcestring · enumOptional

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.

Default: workspacePossible values:
Responses
200

LayerLibrary

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
get
/api/v2/library
GET /api/v2/library HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
  "layer_groups": [
    {
      "caption": "text",
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "layers": [
        {
          "caption": "text",
          "geometry_type": "Line",
          "hide_from_legend": true,
          "id": "luCHyMruTQ6ozGk3gPJfEB",
          "is_spreadsheet": true,
          "legend_display": "default",
          "legend_visibility": "hide",
          "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"
        }
      ],
      "legend_visibility": "hide",
      "links": {
        "self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6D"
      },
      "name": "text",
      "ordering_key": 1,
      "type": "layer_group",
      "visibility_interaction": "default"
    }
  ],
  "layers": [
    {
      "caption": "text",
      "geometry_type": "Line",
      "hide_from_legend": true,
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "is_spreadsheet": true,
      "legend_display": "default",
      "legend_visibility": "hide",
      "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"
}

Delete map element

delete

Permanently delete an element from a map.

This action cannot be undone. The element will be permanently removed from the map.

Authorizations
Path parameters
map_idstringRequired

The ID of the map to delete the element from.

element_idstringRequired

The ID of the element to delete.

Responses
204

No Content

No content

401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
delete
/api/v2/maps/{map_id}/elements/{element_id}
DELETE /api/v2/maps/{map_id}/elements/{element_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*

No content

List map elements

get

Returns a GeoJSON FeatureCollection containing all the elements in a map that are not in an element group.

Authorizations
Path parameters
map_idstringRequired

The ID of the map to list elements from.

Responses
200

GeoJSON

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
get
/api/v2/maps/{map_id}/elements
GET /api/v2/maps/{map_id}/elements HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
  "features": [
    {
      "geometry": {
        "felt:id": "luCHyMruTQ6ozGk3gPJfEB",
        "felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
      },
      "properties": {},
      "type": "Feature"
    }
  ],
  "type": "FeatureCollection"
}

Create or update map elements

post

Create new elements or update existing ones on a map using GeoJSON data. Each element is represented by a feature in the POST'ed GeoJSON Feature Collection. For each feature, including an existing element id will result in the element being updated on the map. If no element id is provided (or a non-existent one), a new element will be created.

The maximum payload size for any POST to the Felt API is 1MB. Additionally, complex element geometry may be automatically simplified. If you require large, complex geometries, consider uploading your data as a Data Layer.

Authorizations
Path parameters
map_idstringRequired

The ID of the map to create the elements in

Body
typestring · enumRequiredPossible values:
Responses
200

GeoJSON

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
post
/api/v2/maps/{map_id}/elements
POST /api/v2/maps/{map_id}/elements HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 165

{
  "features": [
    {
      "geometry": {
        "felt:id": "luCHyMruTQ6ozGk3gPJfEB",
        "felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
      },
      "properties": {},
      "type": "Feature"
    }
  ],
  "type": "FeatureCollection"
}
{
  "features": [
    {
      "geometry": {
        "felt:id": "luCHyMruTQ6ozGk3gPJfEB",
        "felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
      },
      "properties": {},
      "type": "Feature"
    }
  ],
  "type": "FeatureCollection"
}

Get map element group

get

Retrieve all elements from a specific group as GeoJSON.

Authorizations
Path parameters
map_idstringRequired

The ID of the map.

group_idstringRequired

The ID of the element group.

Responses
200

GeoJSON

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
get
/api/v2/maps/{map_id}/element_groups/{group_id}
GET /api/v2/maps/{map_id}/element_groups/{group_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
  "features": [
    {
      "geometry": {
        "felt:id": "luCHyMruTQ6ozGk3gPJfEB",
        "felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
      },
      "properties": {},
      "type": "Feature"
    }
  ],
  "type": "FeatureCollection"
}

List map element groups

get

Returns a list of GeoJSON FeatureCollections, one for each element group in the map.

Authorizations
Path parameters
map_idstringRequired

The ID of the map to list groups from.

Responses
200

ElementGroupList

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
get
/api/v2/maps/{map_id}/element_groups
GET /api/v2/maps/{map_id}/element_groups HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
[
  {
    "color": "text",
    "elements": {
      "features": [
        {
          "geometry": {
            "felt:id": "luCHyMruTQ6ozGk3gPJfEB",
            "felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
          },
          "properties": {},
          "type": "Feature"
        }
      ],
      "type": "FeatureCollection"
    },
    "id": "text",
    "name": "text",
    "symbol": "text"
  }
]

Create or update map element groups

post

Create new element groups or update existing ones.

For each Element Group, including an existing Element Group id will result in the Element Group being updated. If no id is provided, a new Element Group will be created.

The maximum payload size for any POST to the Felt API is 1MB. Additionally, complex element geometry may be automatically simplified. If you require large, complex geometries, consider uploading your data as a Data Layer.

Authorizations
Path parameters
map_idstringRequired

The ID of the map to create the group in

Bodyobject · ElementGroupParams[]
colorstringOptionalDefault: #C93535
idstring · felt_idOptionalExample: luCHyMruTQ6ozGk3gPJfEB
namestringRequiredExample: My Element Group
symbolstringOptionalDefault: dot
Responses
200

Element group list

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
post
/api/v2/maps/{map_id}/element_groups
POST /api/v2/maps/{map_id}/element_groups HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 92

[
  {
    "color": "#C93535",
    "id": "luCHyMruTQ6ozGk3gPJfEB",
    "name": "My Element Group",
    "symbol": "dot"
  }
]
[
  {
    "color": "text",
    "elements": {
      "features": [
        {
          "geometry": {
            "felt:id": "luCHyMruTQ6ozGk3gPJfEB",
            "felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
          },
          "properties": {},
          "type": "Feature"
        }
      ],
      "type": "FeatureCollection"
    },
    "id": "text",
    "name": "text",
    "symbol": "text"
  }
]

Add layer from data source

post

Create a new layer from an existing data source connection (database, API, or file).

Authorizations
Path parameters
map_idstringRequired
Body
one ofOptional
or
or
Responses
202

AddSourceLayerAccepted

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
post
/api/v2/maps/{map_id}/add_source_layer
POST /api/v2/maps/{map_id}/add_source_layer HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 56

{
  "dataset_id": "luCHyMruTQ6ozGk3gPJfEB",
  "from": "dataset"
}
{
  "links": {
    "layer_group": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/KFFhKAbvS4anD3wxtwNEpD",
    "self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC"
  },
  "status": "accepted"
}

Refresh map layer

post

Trigger a data refresh for a layer from its original data source to pull in the latest updates.

After uploading a file or URL, you may want to update the resulting layer with new data. The process is quite similar to the upload:

  • For URL uploads, simply making a single POST request to the refresh endpoint is enough

  • For file refreshes, the response of the initial POST request will include a URL and some pre-signed attributes, which will be used to upload the new file to Amazon S3.

With the felt_python library, you can refresh a layer with a simple function call:

from felt_python import (refresh_file_layer, refresh_url_layer)

refresh_file_layer(map_id, layer_id, file_name="features.geojson")
refresh_url_layer(map_id, layer_id)
Authorizations
Path parameters
map_idstringRequired

The ID of the map hosting the layer to refresh

layer_idstringRequired

The ID of the layer to refresh

Responses
200

Refresh response

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
post
/api/v2/maps/{map_id}/layers/{layer_id}/refresh
POST /api/v2/maps/{map_id}/layers/{layer_id}/refresh HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
  "layer_group_id": "luCHyMruTQ6ozGk3gPJfEB",
  "layer_id": "luCHyMruTQ6ozGk3gPJfEB",
  "presigned_attributes": {},
  "type": "upload_response",
  "url": "text"
}

Upload map layer

post

Upload a file or import data from a URL to create a new layer on the map.

The /upload endpoint can be used for both URL and file uploads:

  • For URL uploads, simply making a single POST request to the upload endpoint is enough

  • For file uploads, the response of the initial POST request contain information you will use to upload the file to Amazon S3

Check our Upload Anything docs to see what URLs are supported.

Uploading the file to Amazon S3

Layer files aren't uploaded directly to the Felt API. Instead, they are uploaded to an S3 bucket.

The response to this API request will include a URL and pre-signed params for you to use to upload your file. Only a single file may be uploaded — if you wish to upload several files at once, consider wrapping them in a zip file.

To upload the file, you must perform a multipart upload, and include the file contents in the file field.

With the felt_python library, you can upload a file with a simple function call:

from felt_python import upload_file

upload_file(map_id, file_name="features.geojson", layer_name="My new layer")
Authorizations
Path parameters
map_idstringRequired

The ID of the map to upload the layer to.

Body
import_urlstringOptional

A public URL containing geodata to import, in place of uploading a file.

latnumberOptional

(Image uploads only) The latitude of the image center.

lngnumberOptional

(Image uploads only) The longitude of the image center.

namestringRequired

The display name for the new layer.

zoomnumberOptional

(Image uploads only) The zoom level of the image.

Responses
200

Upload layer response

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
post
/api/v2/maps/{map_id}/upload
POST /api/v2/maps/{map_id}/upload HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 311

{
  "hints": [
    {
      "attributes": {
        "lat": "text",
        "lng": "text"
      }
    }
  ],
  "import_url": "text",
  "lat": 1,
  "lng": 1,
  "metadata": {
    "attribution_text": "text",
    "attribution_url": "text",
    "description": "text",
    "license": "text",
    "source_abbreviation": "text",
    "source_name": "text",
    "source_url": "text",
    "updated_at": "2025-03-24"
  },
  "name": "text",
  "zoom": 1
}
{
  "layer_group_id": "luCHyMruTQ6ozGk3gPJfEB",
  "layer_id": "luCHyMruTQ6ozGk3gPJfEB",
  "presigned_attributes": {},
  "type": "upload_response",
  "url": "text"
}

Create layer export link

get

Generate a direct download link for layer data export.

Get a link to export a layer as a GeoPackage (vector layers) or GeoTIFF (raster layers).

Authorizations
Path parameters
map_idstringRequired

The ID of the map where the layer is located

layer_idstringRequired

The ID of the layer to export

Responses
200

Export link

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
503

ServiceUnavailableError

application/json
get
/api/v2/maps/{map_id}/layers/{layer_id}/get_export_link
GET /api/v2/maps/{map_id}/layers/{layer_id}/get_export_link HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
  "export_link": "text"
}

Check custom export status

get

Check the processing status and download availability of a custom export request.

If the export is successful, the response will include a download_url for accessing the exported data.

Authorizations
Path parameters
map_idstringRequired

The ID of the map where the layer is located

layer_idstringRequired

The ID of the layer to export

export_idstringRequired

The ID of the export

Responses
200

Custom export request status

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
get
/api/v2/maps/{map_id}/layers/{layer_id}/custom_exports/{export_id}
GET /api/v2/maps/{map_id}/layers/{layer_id}/custom_exports/{export_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
  "download_url": "https://us1.data-pipeline.felt.com/fcdfd96c-06fa-40b9-9ae9-ad034b5a66df/Felt-Export.zip",
  "export_id": "FZWQjWZJSZWvW3yn9BeV9AyA",
  "filters": [],
  "status": "completed"
}

Create custom layer export

post

Start a custom export with specific format and filter options for layer data.

Export requests are asynchronous. A successful response will return a poll_endpoint to check the status of the export using the poll custom export endpoint.

Authorizations
Path parameters
map_idstringRequired

The ID of the map where the layer is located

layer_idstringRequired

The ID of the layer to export

Body
email_on_completionbooleanOptional

Send an email to the requesting user when the export completes. Defaults to true

output_formatstring · enumRequiredExample: csvPossible values:
Responses
200

Custom export response

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
503

ServiceUnavailableError

application/json
post
/api/v2/maps/{map_id}/layers/{layer_id}/custom_export
POST /api/v2/maps/{map_id}/layers/{layer_id}/custom_export HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 63

{
  "email_on_completion": true,
  "filters": [],
  "output_format": "csv"
}
{
  "export_request_id": "luCHyMruTQ6ozGk3gPJfEB",
  "poll_endpoint": "http://felt.com/api/v2/maps/vAbZ5eKqRoGe4sCH8nHW8D/layers/7kF9Cfz45TUWIiuuWV8uZ7A/custom_exports/auFxn9BO4RrGGiKrGfaS7ZB"
}

Update source

post

Update data source connection settings, access permissions, or configuration details.

Connecting the Source and inspecting its datasets will happen asynchronously after the API response is returned. To determine when the inspection process has completed, poll the Show Source endpoint and check for sync_status: completed.

Authorizations
Path parameters
source_idstringRequired

The ID of the source to update

Body
connectionone ofOptional
or
or
or
or
or
or
or
or
or
or
or
or
namestringOptional
permissionsone ofOptional
or
or
Responses
202

Source reference

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
post
/api/v2/sources/{source_id}/update
POST /api/v2/sources/{source_id}/update HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 119

{
  "connection": {
    "blob_storage_url": "text",
    "type": "abs_bucket"
  },
  "name": "text",
  "permissions": {
    "type": "workspace_editors"
  }
}
{
  "automatic_sync": "enabled",
  "connection_type": "abs_bucket",
  "created_at": 1,
  "id": "luCHyMruTQ6ozGk3gPJfEB",
  "last_synced_at": 1,
  "links": {
    "self": "https://felt.com/api/v2/sources/V0dnOMOuTd9B9BOsL9C0UjmqC"
  },
  "name": "text",
  "owner_id": "luCHyMruTQ6ozGk3gPJfEB",
  "permissions": {
    "type": "workspace_editors"
  },
  "sync_status": "syncing",
  "type": "source_reference",
  "updated_at": 1,
  "workspace_id": "luCHyMruTQ6ozGk3gPJfEB"
}

List sources

get

Retrieve all data sources accessible to the authenticated user within the workspace.

Authorizations
Query parameters
workspace_idstringOptional

Only needed when using the API as part of a plugin

Responses
200

Source references

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
get
/api/v2/sources
GET /api/v2/sources HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
[
  {
    "automatic_sync": "enabled",
    "connection_type": "abs_bucket",
    "created_at": 1,
    "id": "luCHyMruTQ6ozGk3gPJfEB",
    "last_synced_at": 1,
    "links": {
      "self": "https://felt.com/api/v2/sources/V0dnOMOuTd9B9BOsL9C0UjmqC"
    },
    "name": "text",
    "owner_id": "luCHyMruTQ6ozGk3gPJfEB",
    "permissions": {
      "type": "workspace_editors"
    },
    "sync_status": "syncing",
    "type": "source_reference",
    "updated_at": 1,
    "workspace_id": "luCHyMruTQ6ozGk3gPJfEB"
  }
]

Create source

post

Create a new data source connection with authentication credentials and access permissions.

Connecting the Source and inspecting its datasets will happen asynchronously after the API response is returned. To determine when the inspection process has completed, poll the Show Source endpoint and check for sync_status: completed.

Authorizations
Body
connectionone ofRequired
or
or
or
or
or
or
or
or
or
or
or
or
namestringRequired
permissionsone ofOptional
or
or
Responses
202

Source reference

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
post
/api/v2/sources
POST /api/v2/sources HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 269

{
  "connection": {
    "blob_storage_url": "text",
    "credentials": [
      {
        "credential": {
          "connection_string": "text",
          "type": "azure_storage_connection_string"
        },
        "name": "text",
        "use_case": "source_authentication"
      }
    ],
    "type": "abs_bucket"
  },
  "name": "text",
  "permissions": {
    "type": "workspace_editors"
  }
}
{
  "automatic_sync": "enabled",
  "connection_type": "abs_bucket",
  "created_at": 1,
  "id": "luCHyMruTQ6ozGk3gPJfEB",
  "last_synced_at": 1,
  "links": {
    "self": "https://felt.com/api/v2/sources/V0dnOMOuTd9B9BOsL9C0UjmqC"
  },
  "name": "text",
  "owner_id": "luCHyMruTQ6ozGk3gPJfEB",
  "permissions": {
    "type": "workspace_editors"
  },
  "sync_status": "syncing",
  "type": "source_reference",
  "updated_at": 1,
  "workspace_id": "luCHyMruTQ6ozGk3gPJfEB"
}

Sync source

post

Trigger a full data synchronization from the source to update all connected layers with latest data.

Syncing will happen asynchronously after the API response is returned. To determine when the inspection process has completed, poll the Show Source endpoint and check for sync_status: completed.

Authorizations
Path parameters
source_idstringRequired

The ID of the source to sync

Responses
202

Source reference

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
post
/api/v2/sources/{source_id}/sync
POST /api/v2/sources/{source_id}/sync HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
  "automatic_sync": "enabled",
  "connection_type": "abs_bucket",
  "created_at": 1,
  "id": "luCHyMruTQ6ozGk3gPJfEB",
  "last_synced_at": 1,
  "links": {
    "self": "https://felt.com/api/v2/sources/V0dnOMOuTd9B9BOsL9C0UjmqC"
  },
  "name": "text",
  "owner_id": "luCHyMruTQ6ozGk3gPJfEB",
  "permissions": {
    "type": "workspace_editors"
  },
  "sync_status": "syncing",
  "type": "source_reference",
  "updated_at": 1,
  "workspace_id": "luCHyMruTQ6ozGk3gPJfEB"
}

Get source

get

Retrieve detailed configuration and connection information for a specific data source.

Authorizations
Path parameters
source_idstringRequired

The ID of the source to show

Responses
200

Source

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
get
/api/v2/sources/{source_id}
GET /api/v2/sources/{source_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
  "automatic_sync": "enabled",
  "connection": {
    "blob_storage_url": "text",
    "credentials": [
      {
        "created_at": 1,
        "credential": {
          "connection_string": "text",
          "type": "azure_storage_connection_string"
        },
        "id": "luCHyMruTQ6ozGk3gPJfEB",
        "name": "text",
        "source_id": "luCHyMruTQ6ozGk3gPJfEB",
        "updated_at": 1,
        "use_case": "source_authentication"
      }
    ],
    "type": "abs_bucket"
  },
  "created_at": 1,
  "datasets": [
    {
      "created_at": 1,
      "description": "text",
      "geometry_type": "polygon",
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "inspection_status": "completed",
      "name": "text",
      "type": "dataset",
      "updated_at": 1
    }
  ],
  "id": "luCHyMruTQ6ozGk3gPJfEB",
  "last_synced_at": 1,
  "name": "text",
  "owner_id": "luCHyMruTQ6ozGk3gPJfEB",
  "permissions": {
    "type": "workspace_editors"
  },
  "sync_status": "syncing",
  "type": "source",
  "updated_at": 1,
  "workspace_id": "luCHyMruTQ6ozGk3gPJfEB"
}

Delete source

delete

Permanently delete a data source connection and all its associated layers and data.

Any layers created from the Source will remain after it is deleted, but they will no longer be refreshed.

Authorizations
Path parameters
source_idstringRequired

The ID of the source to delete

Responses
204

No Content

No content

401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
delete
/api/v2/sources/{source_id}
DELETE /api/v2/sources/{source_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*

No content

Create source credential

post

Add authentication credentials to an existing data source for secure access.

Some sources may need to be configured with additional credentials to work with Felt. Access to S3 Buckets, for example, may be protected by IAM policies. Adding a SourceCredential-AwsAssumeRole credential to your S3 Bucket source allows Felt to connect to a private source.

Sensitive fields in credentials, like SourceCredential-KeyPair.private_key, will be returned as felt:redacted.

Authorizations
Path parameters
source_idstringRequired

The ID of the source to attach the credential

Body
credentialone ofRequired
or
or
or
or
or
namestringRequired
use_casestring · enumRequiredPossible values:
Responses
202

Source credential created

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
post
/api/v2/sources/{source_id}/credentials
POST /api/v2/sources/{source_id}/credentials HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 137

{
  "credential": {
    "role_arn": "text",
    "role_session_name": "text",
    "type": "aws_assume_role"
  },
  "name": "text",
  "use_case": "stac_api_authentication"
}
{
  "created_at": 1,
  "credential": {
    "role_arn": "text",
    "role_session_name": "text",
    "type": "aws_assume_role"
  },
  "id": "luCHyMruTQ6ozGk3gPJfEB",
  "name": "text",
  "source_id": "luCHyMruTQ6ozGk3gPJfEB",
  "updated_at": 1,
  "use_case": "stac_api_authentication"
}

Update source credential

post

Update existing authentication credentials for a data source connection.

Sensitive fields in credentials, like SourceCredential-KeyPair.private_key, will be returned as felt:redacted.

Authorizations
Path parameters
source_idstringRequired

The ID of the source that the credential belongs to

credential_idstringRequired

The ID of the credential

Body
credentialone ofOptional
or
or
or
or
or
namestringOptional
use_casestring · enumOptionalPossible values:
Responses
202

Source credential updated

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
post
/api/v2/sources/{source_id}/credentials/{credential_id}/update
POST /api/v2/sources/{source_id}/credentials/{credential_id}/update HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 137

{
  "credential": {
    "role_arn": "text",
    "role_session_name": "text",
    "type": "aws_assume_role"
  },
  "name": "text",
  "use_case": "stac_api_authentication"
}
{
  "created_at": 1,
  "credential": {
    "role_arn": "text",
    "role_session_name": "text",
    "type": "aws_assume_role"
  },
  "id": "luCHyMruTQ6ozGk3gPJfEB",
  "name": "text",
  "source_id": "luCHyMruTQ6ozGk3gPJfEB",
  "updated_at": 1,
  "use_case": "stac_api_authentication"
}

Delete source credential

delete

Remove authentication credentials from a data source connection.

Authorizations
Path parameters
source_idstringRequired

The ID of the source that the credential belongs to

credential_idstringRequired

The ID of the credential to delete

Responses
204

No Content

No content

401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
delete
/api/v2/sources/{source_id}/credentials/{credential_id}
DELETE /api/v2/sources/{source_id}/credentials/{credential_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*

No content

Export map comments

get

Export all comments and replies from a map in CSV or JSON format.

Authorizations
Path parameters
map_idstringRequired

The ID of the map to export comments from.

Query parameters
formatstringOptional

The format to export the comments in, either 'csv' or 'json' (default)

Responses
200

Comment export response

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
get
/api/v2/maps/{map_id}/comments/export
GET /api/v2/maps/{map_id}/comments/export HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
[]

Resolve map comment

post

Mark a comment thread as resolved.

Authorizations
Path parameters
map_idstringRequired

The ID of the map that contains the comment.

comment_idstringRequired

The ID of the comment to resolve.

Responses
200

Comment resolved response

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
post
/api/v2/maps/{map_id}/comments/{comment_id}/resolve
POST /api/v2/maps/{map_id}/comments/{comment_id}/resolve HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
  "comment_id": "luCHyMruTQ6ozGk3gPJfEB"
}

Delete map comment

delete

Permanently delete a comment or reply from the map.

This action cannot be undone. The comment or reply will be permanently removed from the map.

Authorizations
Path parameters
map_idstringRequired

The ID of the map that contains the comment.

comment_idstringRequired

The ID of the comment to delete.

Responses
204

No Content

No content

401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
delete
/api/v2/maps/{map_id}/comments/{comment_id}
DELETE /api/v2/maps/{map_id}/comments/{comment_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*

No content

Create an Embed Token

post

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. Each end user should be a member of your Felt workspace with a viewer, editor, or admin role assigned.

Usage

  • 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>

Enabling Layer Export

You can allow EmbedToken based page views to export layer data.

  • Turn on "Viewer permissions: Export data" in Map settings

Authorizations
Path parameters
map_idstringRequired
Query parameters
user_emailstringRequired

Each token must be associated with the email address of the user who will use it.

Responses
200

EmbedToken

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
post
/api/v2/maps/{map_id}/embed_token
POST /api/v2/maps/{map_id}/embed_token?user_email=text HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
  "expires_at": "2024-05-25T15:51:34",
  "token": "text"
}

Get current user

get

Retrieve profile information and settings for the authenticated user.

Authorizations
Responses
200

User

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
get
/api/v2/user
GET /api/v2/user HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
  "email": "text",
  "id": "luCHyMruTQ6ozGk3gPJfEB",
  "name": "text"
}

Get map layer group

get

Retrieve detailed information about a specific layer group including its layers and configuration.

Authorizations
Path parameters
map_idstringRequired
layer_group_idstringRequired
Responses
200

Layer Group

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
get
/api/v2/maps/{map_id}/layer_groups/{layer_group_id}
GET /api/v2/maps/{map_id}/layer_groups/{layer_group_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
  "caption": "text",
  "id": "luCHyMruTQ6ozGk3gPJfEB",
  "layers": [
    {
      "caption": "text",
      "geometry_type": "Line",
      "hide_from_legend": true,
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "is_spreadsheet": true,
      "legend_display": "default",
      "legend_visibility": "hide",
      "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"
    }
  ],
  "legend_visibility": "hide",
  "links": {
    "self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6D"
  },
  "name": "text",
  "ordering_key": 1,
  "type": "layer_group",
  "visibility_interaction": "default"
}

Update map layer group

post

Update layer group properties including name, visibility, and organization settings.

Authorizations
Path parameters
map_idstringRequired
layer_group_idstringRequired
Body
captionstringOptionalExample: A very interesting group
idstring · felt_idOptionalExample: luCHyMruTQ6ozGk3gPJfEB
legend_visibilitystring · enumOptional

Controls how the layer group is displayed in the legend

Possible values:
namestringOptionalExample: My Layer Group
ordering_keyintegerOptional
subtitlestringOptionalDeprecated

Deprecated: use caption instead.

visibility_interactionstring · enum | nullableOptional

Controls how the layer group is displayed in the legend. Defaults to "default".

Possible values:
Responses
200

LayerGroup

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
post
/api/v2/maps/{map_id}/layer_groups/{layer_group_id}
POST /api/v2/maps/{map_id}/layer_groups/{layer_group_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 171

{
  "caption": "A very interesting group",
  "id": "luCHyMruTQ6ozGk3gPJfEB",
  "legend_visibility": "hide",
  "name": "My Layer Group",
  "ordering_key": 1,
  "visibility_interaction": "default"
}
{
  "caption": "text",
  "id": "luCHyMruTQ6ozGk3gPJfEB",
  "layers": [
    {
      "caption": "text",
      "geometry_type": "Line",
      "hide_from_legend": true,
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "is_spreadsheet": true,
      "legend_display": "default",
      "legend_visibility": "hide",
      "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"
    }
  ],
  "legend_visibility": "hide",
  "links": {
    "self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6D"
  },
  "name": "text",
  "ordering_key": 1,
  "type": "layer_group",
  "visibility_interaction": "default"
}

Delete map layer group

delete

Permanently remove a layer group and all its contained layers from a map.

This action cannot be undone. The layer group and all its contained layers will be permanently removed from the map.

Authorizations
Path parameters
map_idstringRequired

The ID of the map to delete the layer group from

layer_group_idstringRequired

The ID of the layer group to delete

Responses
204

No Content

No content

401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
delete
/api/v2/maps/{map_id}/layer_groups/{layer_group_id}
DELETE /api/v2/maps/{map_id}/layer_groups/{layer_group_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*

No content

List map layers

get

Retrieve all layers from a map, including uploaded files and connected data sources.

Authorizations
Path parameters
map_idstringRequired
Responses
200

Layers list

application/json
401

UnauthorizedError

application/json
404

NotFoundError

application/json
500

InternalServerError

application/json
get
/api/v2/maps/{map_id}/layers
GET /api/v2/maps/{map_id}/layers HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
[
  {
    "caption": "text",
    "geometry_type": "Line",
    "hide_from_legend": true,
    "id": "luCHyMruTQ6ozGk3gPJfEB",
    "is_spreadsheet": true,
    "legend_display": "default",
    "legend_visibility": "hide",
    "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"
  }
]

Update map layer

post

Update layer properties including styling, visibility, grouping, and other configuration options.

Authorizations
Path parameters
map_idstringRequired
Bodyobject · LayerUpdateParams[]
captionstringOptionalExample: A very interesting dataset
idstring · felt_idRequiredExample: luCHyMruTQ6ozGk3gPJfEB
layer_group_idstring · felt_id | nullableOptionalExample: luCHyMruTQ6ozGk3gPJfEB
legend_displaystring · enumOptional

Controls how the layer is displayed in the legend.

Possible values:
legend_visibilitystring · enumOptional

Controls whether or not the layer is displayed in the legend.

Possible values:
namestringOptionalExample: My Layer
ordering_keyintegerOptional
refresh_periodstring · enumOptionalPossible values:
subtitlestringOptionalDeprecated

Deprecated: use caption instead.

Responses
200

Layer list

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
post
/api/v2/maps/{map_id}/layers
POST /api/v2/maps/{map_id}/layers HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 427

[
  {
    "caption": "A very interesting dataset",
    "id": "luCHyMruTQ6ozGk3gPJfEB",
    "layer_group_id": "luCHyMruTQ6ozGk3gPJfEB",
    "legend_display": "default",
    "legend_visibility": "hide",
    "metadata": {
      "attribution_text": "text",
      "attribution_url": "text",
      "description": "text",
      "license": "text",
      "source_abbreviation": "text",
      "source_name": "text",
      "source_url": "text",
      "updated_at": "2025-03-24"
    },
    "name": "My Layer",
    "ordering_key": 1,
    "refresh_period": "15 min"
  }
]
[
  {
    "caption": "text",
    "geometry_type": "Line",
    "hide_from_legend": true,
    "id": "luCHyMruTQ6ozGk3gPJfEB",
    "is_spreadsheet": true,
    "legend_display": "default",
    "legend_visibility": "hide",
    "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"
  }
]

List map layer groups

get

Retrieve all layer groups from a map to see how layers are organized.

Authorizations
Path parameters
map_idstringRequired
Responses
200

Layers Groups

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
get
/api/v2/maps/{map_id}/layer_groups
GET /api/v2/maps/{map_id}/layer_groups HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
[
  {
    "caption": "text",
    "id": "luCHyMruTQ6ozGk3gPJfEB",
    "layers": [
      {
        "caption": "text",
        "geometry_type": "Line",
        "hide_from_legend": true,
        "id": "luCHyMruTQ6ozGk3gPJfEB",
        "is_spreadsheet": true,
        "legend_display": "default",
        "legend_visibility": "hide",
        "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"
      }
    ],
    "legend_visibility": "hide",
    "links": {
      "self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6D"
    },
    "name": "text",
    "ordering_key": 1,
    "type": "layer_group",
    "visibility_interaction": "default"
  }
]

Update map layer groups

post

Update properties for multiple layer groups in a single request for efficient bulk operations.

Authorizations
Path parameters
map_idstringRequired
Bodyobject · LayerGroupParams[]
captionstringOptionalExample: A very interesting group
idstring · felt_idOptionalExample: luCHyMruTQ6ozGk3gPJfEB
legend_visibilitystring · enumOptional

Controls how the layer group is displayed in the legend

Possible values:
namestringRequiredExample: My Layer Group
ordering_keyintegerOptional
subtitlestringOptionalDeprecated

Deprecated: use caption instead.

visibility_interactionstring · enum | nullableOptional

Controls how the layer group is displayed in the legend. Defaults to "default".

Possible values:
Responses
200

LayerGroup list

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
post
/api/v2/maps/{map_id}/layer_groups
POST /api/v2/maps/{map_id}/layer_groups HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 173

[
  {
    "caption": "A very interesting group",
    "id": "luCHyMruTQ6ozGk3gPJfEB",
    "legend_visibility": "hide",
    "name": "My Layer Group",
    "ordering_key": 1,
    "visibility_interaction": "default"
  }
]
[
  {
    "caption": "text",
    "id": "luCHyMruTQ6ozGk3gPJfEB",
    "layers": [
      {
        "caption": "text",
        "geometry_type": "Line",
        "hide_from_legend": true,
        "id": "luCHyMruTQ6ozGk3gPJfEB",
        "is_spreadsheet": true,
        "legend_display": "default",
        "legend_visibility": "hide",
        "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"
      }
    ],
    "legend_visibility": "hide",
    "links": {
      "self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6D"
    },
    "name": "text",
    "ordering_key": 1,
    "type": "layer_group",
    "visibility_interaction": "default"
  }
]

Update layer style

post

Update the visual styling properties of a layer including colors, symbols, and rendering options.

Authorizations
Path parameters
map_idstringRequired

The ID of the map where the layer is located

layer_idstringRequired

The ID of the layer to update the style of

Body
styleobjectRequired

The new layer style, specified in Felt Style Language format

Responses
200

Layer

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
post
/api/v2/maps/{map_id}/layers/{layer_id}/update_style
POST /api/v2/maps/{map_id}/layers/{layer_id}/update_style HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 12

{
  "style": {}
}
{
  "caption": "text",
  "geometry_type": "Line",
  "hide_from_legend": true,
  "id": "luCHyMruTQ6ozGk3gPJfEB",
  "is_spreadsheet": true,
  "legend_display": "default",
  "legend_visibility": "hide",
  "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 map layer

get

Retrieve detailed information about a specific layer including data source, styling, and configuration.

Authorizations
Path parameters
map_idstringRequired
layer_idstringRequired
Responses
200

Layer

application/json
401

UnauthorizedError

application/json
404

NotFoundError

application/json
500

InternalServerError

application/json
get
/api/v2/maps/{map_id}/layers/{layer_id}
GET /api/v2/maps/{map_id}/layers/{layer_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
  "caption": "text",
  "geometry_type": "Line",
  "hide_from_legend": true,
  "id": "luCHyMruTQ6ozGk3gPJfEB",
  "is_spreadsheet": true,
  "legend_display": "default",
  "legend_visibility": "hide",
  "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"
}

Delete map layer

delete

Permanently remove a layer from a map.

This action cannot be undone. The layer and all its data will be permanently removed from the map.

Authorizations
Path parameters
map_idstringRequired

The ID of the map to delete the layer from

layer_idstringRequired

The ID of the layer to delete

Responses
204

No Content

No content

401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
delete
/api/v2/maps/{map_id}/layers/{layer_id}
DELETE /api/v2/maps/{map_id}/layers/{layer_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*

No content

Duplicate map layers

post

Copy layers or layer groups to other maps, preserving styling and configuration.

Authorizations
Bodyone of[]
itemsone ofOptional
or
Responses
200

Duplicate Layers Response

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
post
/api/v2/duplicate_layers
POST /api/v2/duplicate_layers HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 92

[
  {
    "destination_map_id": "luCHyMruTQ6ozGk3gPJfEB",
    "source_layer_id": "luCHyMruTQ6ozGk3gPJfEB"
  }
]
{
  "layer_groups": [
    {
      "caption": "text",
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "layers": [
        {
          "caption": "text",
          "geometry_type": "Line",
          "hide_from_legend": true,
          "id": "luCHyMruTQ6ozGk3gPJfEB",
          "is_spreadsheet": true,
          "legend_display": "default",
          "legend_visibility": "hide",
          "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"
        }
      ],
      "legend_visibility": "hide",
      "links": {
        "self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC/layer_groups/v13k4Ae9BRjCHHdPP5Fcm6D"
      },
      "name": "text",
      "ordering_key": 1,
      "type": "layer_group",
      "visibility_interaction": "default"
    }
  ],
  "layers": [
    {
      "caption": "text",
      "geometry_type": "Line",
      "hide_from_legend": true,
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "is_spreadsheet": true,
      "legend_display": "default",
      "legend_visibility": "hide",
      "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"
    }
  ]
}

List projects

get

Retrieve all projects accessible to the authenticated user within the workspace.

Authorizations
Query parameters
workspace_idstringOptional

Only needed when using the API as part of a plugin

Responses
200

Projects

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
get
/api/v2/projects
GET /api/v2/projects HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
[
  {
    "id": "luCHyMruTQ6ozGk3gPJfEB",
    "links": {
      "self": "https://felt.com/api/v2/projects/V0dnOMOuTd9B9BOsL9C0UjmqC"
    },
    "name": "text",
    "type": "project_reference",
    "visibility": "workspace"
  }
]

Create project

post

Create a new project with specified name and visibility settings within the workspace.

Authorizations
Body
namestringRequired

The name to be used for the Project

visibilitystring · enumRequired

Either viewable by all members of the workspace, or private to users who are invited.

Possible values:
Responses
200

Project

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
post
/api/v2/projects
POST /api/v2/projects HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
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"
}

Get project

get

Retrieve detailed information about a specific project including metadata, permissions, and references to the maps in the project.

Authorizations
Path parameters
project_idstringRequired
Responses
200

Project

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
get
/api/v2/projects/{project_id}
GET /api/v2/projects/{project_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
  "id": "luCHyMruTQ6ozGk3gPJfEB",
  "maps": [
    {
      "created_at": "2024-05-25T15:51:34",
      "folder_id": "text",
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "links": {
        "self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC"
      },
      "project_id": "text",
      "public_access": "private",
      "thumbnail_url": "text",
      "title": "text",
      "type": "map_reference",
      "url": "text",
      "visited_at": "text"
    }
  ],
  "name": "text",
  "type": "project",
  "visibility": "workspace"
}

Delete project

delete

Permanently delete a project and all its contained maps and folders.

Caution: Deleting a project deletes all of the folders and maps inside!

Authorizations
Path parameters
project_idstringRequired

The ID of the Project to delete. Note: This will delete all Folders and Maps inside the project!

Responses
204

No Content

No content

401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
delete
/api/v2/projects/{project_id}
DELETE /api/v2/projects/{project_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*

No content

Update project

post

Update project properties including name and visibility settings.

Authorizations
Path parameters
project_idstringRequired

The ID of the project to update

Body
namestringOptional

The name to be used for the Project

visibilitystring · enumOptional

Either viewable by all members of the workspace, or private to users who are invited.

Possible values:
Responses
200

Project

application/json
401

UnauthorizedError

application/json
403

UnauthorizedError

application/json
404

NotFoundError

application/json
422

Unprocessable Entity

application/json
429

Unprocessable Entity

application/json
500

InternalServerError

application/json
post
/api/v2/projects/{project_id}/update
POST /api/v2/projects/{project_id}/update HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 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"
}