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

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 REST API allows editors to interact with the Felt platform via code, performing actions such as creating new maps, adding data to maps, styling layers, and more. The REST API can be leveraged from any environment that is capable of sending GET and POST requests.

For Python users, interactions with the REST API are simplified through the felt-python module, which can be installed with pip and used to call the REST API endpoints directly from Python functions.

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

API Reference

Layer Uploads

APIs to upload data

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

Layer Library

APIs to publish layers

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

Layer Exports

APIs to export layer data

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

Users

APIs for user information

Users represent the people in your workspace.

With these APIs, you can retrieve user profile information.

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:

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

// Get all layers
const layers = await felt.getLayers();

Using constraints

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

// Get elements with specific IDs
const elements = await felt.getElements({
  ids: ["element-1", "element-2"]
});

// Get legend items belonging to a layer
const legendItems = await felt.getLegendItems({
  layerIds: ["layer-1"],
});

Reacting to changes

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

felt.onLayerChange({
  options: {id: "layer-1"},
  handler: ({layer}) => {
    console.log(layer.visible);
  }
});

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:

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");
}
  1. Batch operations: When you need multiple entities, use the bulk methods (getLayers, getElements, etc.) with constraints rather than making multiple individual calls:

// Better approach
const layers = await felt.getLayers({ ids: ["layer-1", "layer-2"] });

// Less efficient approach
const layer1 = await felt.getLayer("layer-1");
const layer2 = await felt.getLayer("layer-2");

This approach to reading and monitoring entities gives you full control over your map's data and allows you to build interactive applications that stay in sync with the map's state.

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.

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.

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.

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.

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.

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:

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.

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:

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:

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:

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:

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.

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.

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

The legend block

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

Take a look at to see how it works.

These are the fields that each legend block can contain:

Field name
Description

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

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

displayName

Optional. How this attribute will be shown in different parts of the Felt UI.

format

Optional. A numbro 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"},
}

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

displayName

Optional. In categorical visualizations, a dictionary that maps from each category to what will be displayed on the legend. In numeric or heatmap 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"
  }
}
the legends section
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
<html>
  <body>
    <h1>My Felt app</h1>
    <div id="container"></div>
  </body>
</html>
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%";
<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",
);
<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
);

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

const viewport = await felt.getViewport();
console.log(viewport.center); // { latitude: number, longitude: number }
console.log(viewport.zoom);   // number

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:

felt.setViewport({
  center: {
    latitude: 37.7749,
    longitude: -122.4194
  },
  zoom: 12
});

Fitting to bounds

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

felt.fitViewportToBounds({
  bounds: [
    west,   // minimum longitude
    south,  // minimum latitude
    east,   // maximum longitude
    north   // maximum latitude
  ]
});

Responding to viewport changes

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

const unsubscribe = felt.onViewportMove({
  handler: (viewport) => {
    console.log("New center:", viewport.center);
    console.log("New zoom:", viewport.zoom);
  }
});

// Clean up when done
unsubscribe();

Map interactions

Click events

Listen for click events on the map using onPointerClick:

const unsubscribe = felt.onPointerClick({
  handler: (event) => {
    // Location of the click
    console.log("Click location:", event.center);
    
    // Features under the click
    console.log("Clicked features:", event.features);
    
    // 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;
  }
});

Hover events

Track mouse movement over the map using onPointerMove:

const unsubscribe = felt.onPointerMove({
  handler: (event) => {
    // Current mouse location
    console.log("Mouse location:", event.center);
    
    // Features under the cursor
    console.log("Hovered features:", event.features);
    
    // 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;
  }
});

Best practices

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

const unsubscribe = felt.onPointerMove({
  handler: (event) => {
    // Handle event...
  }
});

// Later, when you're done:
unsubscribe();
  1. Throttling: For pointer move events, consider throttling your handler if you're doing expensive operations:

import { throttle } from "lodash";

felt.onPointerMove({
  handler: throttle((event) => {
    // Handle frequent mouse moves...
  }, 100) // Limit to once every 100ms
});

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

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

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

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.

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.

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

version

Mandatory. Defines which version this style adheres to

type

Optional. One of , , or . Defaults to simple.

config

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

style

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

label

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

legend

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

popup

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

attributes

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

filters

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

{
  "version": "2.1",
  "type": "simple",
  "style": {...}
}

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.

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

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.

The following map is an example of a raster layer using a hillshade visualization

with the following style

{
  "version": "2.3",
  "type": "hillshade",
  "config": {"band": 1},
  "legend": {},
  "paint": {"isSandwiched": false}
}

Felt also supports adding color to hillshades by defining a color property in the style

which is defined by the following style

{
  "config": {"band": 1, "steps": [-154.46435546875, 7987.457987843631]},
  "legend": {},
  "type": "hillshade",
  "version": "2.3",
  "paint": {
    "isSandwiched": false,
    "color": "@feltHeat",
    "source": 315,
    "intensity": 0.76
  }
}

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 the full specification.

We support multiple types of interpolators: Step functions, linear, exponential and cubic bezier to enable your map looking like you want at each zoom level. See the Interpolators page.

An example of a layer changing feature colors depending on the zoom level can be found below

"paint": {
  "color": {"linear": [[14, "red"], [20, "blue"]]},
  ...
}

On zoom levels lower than 14, features of this layer will be rendered in red color. On zoom levels higher than 20, features of this layer will be rendered in blue color.

In zooms between 14 and 20, color will be linearly interpolated between red and blue.

Features at zoom level 14
Features at zoom level 17
Features at zoom level 20

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 :

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:

2. Upload your file(s) to Amazon s3

Monitoring progress

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

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.

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:

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.

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:

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:

Layers

Control visibility of layer groups using setLayerVisibility:

Layer groups

Control visibility of layer groups using setLayerGroupVisibility:

Element groups

Similarly, control element group visibility with setElementGroupVisibility:

Legend items

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

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:

Toggling visibility

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

Best practices

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

  1. Omit unused properties: When you only need to show or hide, omit the unused property rather than including it with an empty array:

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

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:

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

const layer = await felt.getLayer("layer-1");
felt.getElements().then(elements => {
  console.log(elements);
});
const layer = await felt.getLayer("layer-1");
const element = await felt.getElement("element-1");
const layers = await felt.getLayers({ ids: ["layer-1", "layer-2"] });
const legendItems = await felt.getLegendItems({ layerIds: ["layer-1", "layer-2"] });
const layers = await felt.getLayers();
const elements = await felt.getLegendItems();
const unsubscribe = felt.onLayerChange({
  options: { id: "layer-1" },
  handler: ({ layer }) => {
    console.log("Layer updated:", layer);
  }
});
const selection = await felt.getSelection();
const unsubscribe = felt.onSelectionChange({
  handler: ({ selection }) => {
    console.log("Selection updated:", selection);
  }
});
const filters = await felt.getLayerFilters("layer-1");
await felt.setLayerFilters({
  layerId: "layer-1",
  filters: ["name", "eq", "Jane"],
});
const unsubscribe = felt.onLayerFiltersChange({
  options: {layerId: "layer-1"},
  handler: (filters) => console.log(filters)
})
const unsubscribe = felt.onLayerChange({
  options: { id: "layer-1" },
  handler: ({ layer }) => {
    console.log("Layer changed:", layer);
  }
});

// Later, when you're done listening:
unsubscribe();
useEffect(() => {
  const unsubscribe = felt.onViewportMove({
    handler: (viewport) => {
      console.log("Viewport changed:", viewport);
    }
  });
  
  // Clean up when the component unmounts
  return () => unsubscribe();
}, []);
// Current API
felt.onElementChange({
  options: { id: "element-1" },
  handler: ({ element }) => { /* ... */ }
});

// If we need to add new options later, no breaking changes:
felt.onElementChange({
  options: { 
    id: "element-1",
    newOption: "value" // Can add new options without breaking existing code
  },
  handler: ({ element }) => { /* ... */ }
});
felt.onSelectionChange({
  handler: ({ selection }) => {
    selection.forEach(node => {
      console.log(node.type);    // e.g., "element", "layer", "feature", ...
      console.log(node.entity);  // The actual entity object
      
      if (node.type === "element") {
        // TypeScript knows this is an Element
        console.log(node.entity.attributes);
      }
    });
  }
});
{
  show?: string[],  // IDs of entities to show
  hide?: string[]   // IDs of entities to hide
}
felt.setLayerVisibility({
  show: ["layer-1", "layer-2"],
  hide: ["layer-3"]
});
felt.setLayerGroupVisibility({
  show: ["group-1", "group-2"],
  hide: ["group-3"]
});
felt.setElementGroupVisibility({
  show: ["points-group"],
  hide: ["lines-group", "polygons-group"]
});
felt.setLegendItemVisibility({
  show: [
    { layerId: "layer-1", id: "item-1" },
    { layerId: "layer-1", id: "item-2" }
  ],
  hide: [
    { layerId: "layer-1", id: "item-3" }
  ]
});
const layers = await felt.getLayers();
const targetLayerId = "important-layer";

felt.setLayerVisibility({
  show: [targetLayerId],
  hide: layers
    .map(layer => layer?.id)
    .filter(id => id && id !== targetLayerId)
});
function toggleLayer(layerId: string, visible: boolean) {
  felt.setLayerVisibility({
    show: visible ? [layerId] : [],
    hide: visible ? [] : [layerId]
  });
}
// Better approach
felt.setLayerVisibility({
  show: ["layer-1", "layer-2"],
  hide: ["layer-3", "layer-4"]
});

// Less efficient approach
felt.setLayerVisibility({ show: ["layer-1"] });
felt.setLayerVisibility({ show: ["layer-2"] });
felt.setLayerVisibility({ hide: ["layer-3"] });
felt.setLayerVisibility({ hide: ["layer-4"] });
// Do this
felt.setLayerVisibility({
  show: ["layer-1"]
});
simple
categorical
numeric
heatmap
Learn more
Learn more.
Learn more.
Learn more.
Learn more.
Learn more.
Learn more.
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
{
  "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}
}

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

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)

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.

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 regular file uploads, 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:

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
)

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

Refreshing a layer with a URL

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

# Your API token and map ID should look like this:
# FELT_API_TOKEN="felt_pat_ABCDEFUDQPAGGNBmX40YNhkCRvvLI3f8/BCwD/g8"
FELT_API_TOKEN="<YOUR_API_TOKEN>"
MAP_ID="<YOUR_MAP_ID>"
LAYER_ID="<YOUR_LAYER_ID>"

curl -L \
  -X POST \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${FELT_API_TOKEN}" \
  "https://felt.com/api/v2/maps/${MAP_ID}/layers/${LAYER_ID}/refresh"
import requests

# Your API token and map ID should look like this:
# api_token = "felt_pat_ABCDEFUDQPAGGNBmX40YNhkCRvvLI3f8/BCwD/g8"
api_token = "<YOUR_API_TOKEN>"
map_id = "<YOUR_MAP_ID>"
layer_id = "<YOUR_LAYER_ID>"

r = requests.post(
  f"http://felt.com/api/v2/maps/{map_id}/layers/{layer_id}/refresh",
  headers={"Authorization": f"Bearer {api_token}"}
)
assert r.ok
print(r.json())
from felt_python import refresh_url_layer

refresh_url_layer(map_id, layer_id)

import os

from felt_python import upload_url

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

map_id = "<YOUR_MAP_ID>"

url_upload = upload_url(
    map_id=map_id,
    layer_url="https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_hour.geojson",
    layer_name="USGS Earthquakes",
)
layer_id = url_upload["layer_id"]
r = requests.post(
    f"https://felt.com/api/v2/maps/{map_id}/upload",
    headers={
        "Authorization": f"Bearer {api_token}",
        "Content-Type": "application/json",
    },
    json={"name": "My new layer"},
)
assert r.ok
layer_id = r.json()["layer_id"]

presigned_upload = r.json()
from felt_python import upload_file

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

upload_file(
  map_id=map_id,
  file_name="YOUR_FILE_WITH_EXTENSION",
  layer_name="My new layer",
)
# This code is a continuation of the previous Python code block
# and assumes you already have a "presigned_upload" variable

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

url = presigned_upload["url"]
presigned_attributes = presigned_upload["presigned_attributes"]
# A 204 response indicates that the upload was successful
with open(file_name, "rb") as file_obj:
    output = requests.post(
        url,
        # Order is important, file should come at the end
        files={**presigned_attributes, "file": file_obj},
    )
# Nothing! Uploading a file is a single step with the felt-python library
curl -L \
  "https://felt.com/api/v2/maps/${MAP_ID}/layers/{LAYER_ID}" \
  -H "Authorization: Bearer ${YOUR_API_TOKEN}"
r = requests.get(
    f"https://felt.com/api/v2/maps/{map_id}/layers/{layer_id}",
    headers={"Authorization": f"Bearer {api_token}"},
)
assert r.ok
print(r.json()["progress"])
from felt_python import get_layer_details

get_layer_details(map_id, layer_id)["progress"]
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"]
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...
  }
});

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 Enterprise plan. Reach out to set up a trial.

Endpoints

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

https://felt.com/api/v2

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 Developers tab of the Workspace Settings page:

You can generate as many API tokens as you need. Make sure to copy them to a secure location!

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:

pip install felt-python

Example: 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

# This looks like:
# api_token = "felt_pat_ABCDEFUDQPAGGNBmX40YNhkCRvvLI3f8/BCwD/g8"
api_token = "<YOUR_API_TOKEN>"

r = requests.post(
  "http://felt.com/api/v2/maps",
  json={"title": "My newly created map"},
  headers={"Authorization": f"Bearer {api_token}"}
)
assert r.ok
map_id = r.json()["id"]

print(r.json())
import os

from felt_python import create_map

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

response = create_map(
    title="My newly created map",
    lat=40,
    lon=-3,
    public_access="private",
)
map_id = response["id"]

Notice in the response the "id" property. Every map has a unique ID, which is also a part of the map's URL. Let's take note of it for future API calls.

Also part of the response is a "url" property, which is the URL to your newly-created map. Feel free to open it! For now, it should just show a blank map.

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 many kinds of file and URL imports. In this case, we'll import all the recent earthquakes from the USGS' live GeoJSON feed:

# Store the map ID from the previous call:
# MAP_ID="CjU1CMJPTAGofjOK3ICf1D"
MAP_ID="<YOUR_MAP_ID>"

curl -L \
  -X POST \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${FELT_API_TOKEN}" \
  "https://felt.com/api/v2/maps/${MAP_ID}/upload" \
  -d '{"import_url":"https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_hour.geojson", "name": "USGS Earthquakes"}'
r = requests.post(
  f"http://felt.com/api/v2/maps/{map_id}/upload",
  json={
    "import_url":"https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_hour.geojson",
    "name": "USGS Earthquakes",
  },
  headers={"Authorization": f"Bearer {api_token}"}
)
assert r.ok
layer_id = r.json()["layer_id"]

print(r.json())
from felt_python import upload_url

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

Like maps, layers also have unique identifiers. Let's take note of this one (also called "id" in the response) so we can style it in the next call.

You can see the uploaded result in your map:

Since we imported a live data feed, the points on your layer may look different.

Example: Styling a layer

Layer styles are defined in the Felt Style Language, 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:

# Store the layer ID from the previous call:
# LAYER_ID="CjU1CMJPTAGofjOK3ICf1D"
LAYER_ID="<YOUR_LAYER_ID>"

curl -L \
  -X POST \
  "https://felt.com/api/v2/maps/${MAP_ID}/layers/${LAYER_ID}/update_style" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${FELT_API_TOKEN}" \
  --data '{"style": {"paint": {"color": "green", "opacity": 0.9, "size": 30, "strokeColor": "auto", "strokeWidth": 1}, "legend": {}, "type": "simple", "version": "2.1"}}'
new_fsl = {
  "paint": {
    "color": "green",
    "opacity": 0.9,
    "size": 30,
    "strokeColor": "auto",
    "strokeWidth": 1
  },
  "legend": {},
  "type": "simple",
  "version": "2.1"
}

r = requests.post(
  f"http://felt.com/api/v2/maps/{map_id}/layers/{layer_id}/update_style",
  json={"style": new_fsl},
  headers={"Authorization": f"Bearer {api_token}"}
)
assert r.ok
print(r.json())
from felt_python import update_layer_style

new_fsl = {
  "paint": {
    "color": "green",
    "opacity": 0.9,
    "size": 30,
    "strokeColor": "auto",
    "strokeWidth": 1
  },
  "legend": {},
  "type": "simple",
  "version": "2.1"
}

update_layer_style(
    map_id=map_id,
    layer_id=layer_id,
    style=new_fsl,
)

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

Since we imported a live data feed, the points on your layer may look different.

Example: Refreshing a live data layer

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

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

curl -L \
  -X POST \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${FELT_API_TOKEN}" \
  "https://felt.com/api/v2/maps/${MAP_ID}/layers/${LAYER_ID}/refresh"
r = requests.post(
  f"https://felt.com/api/v2/maps/{map_id}/layers/{layer_id}/refresh",
  headers={"Authorization": f"Bearer {api_token}"}
)
assert r.ok
print(r.json())
from felt_python import refresh_url_layer

refresh_url_layer(map_id, layer_id)

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

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

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
  }
}
Authentication
Graph showing a Step interpolator function
Graph showing a Linear interpolator function

Working with elements

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

Combining elements with webhooks 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 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())
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)

Listing all element groups

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

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)

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.

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)

Delete an element

Elements can be deleted by referencing them by ID

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)

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

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.

Default Values

Name
Points
Polygons
Lines
Raster

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

-

-

Examples

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

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:

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

From a local file

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

// Assuming you have a File object from a file input
const fileInput = document.getElementById('geojson-upload');
const file = fileInput.files[0];

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

if (layerResult) {
  // Store the layer ID for later reference
  const layerId = layerResult.layers[0].id;
}

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:

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

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:

const layerResult = await felt.createLayersFromGeoJson({
  name: "Styled Features",
  source: {
    type: "geoJsonUrl",
    url: "https://example.com/data/mixed-features.geojson"
  },
  geometryStyles: {
    Point: {
      paint: { 
        color: "red", 
        size: 8 
      }
    },
    Line: {
      paint: { 
        color: "blue", 
        size: 4 
      },
      config: { 
        labelAttribute: ["name"] 
      },
      label: { 
        minZoom: 0 
      }
    },
    Polygon: {
      paint: { 
        color: "green", 
        strokeColor: "darkgreen",
        fillOpacity: 0.5
      }
    }
  }
});

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

Deleting layers

To remove a GeoJSON layer:

await felt.deleteLayer("layer-1");

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

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.

const layerResult = await felt.createLayersFromGeoJson({
  source: {
    type: "geoJsonUrl",
    url: "https://example.com/data/realtime-sensors.geojson",
    refreshInterval: 60000  // Refresh every minute
  },
  name: "Live Sensor Data"
});

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

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.

2. Categories: group by values

Group features by unique attribute values and calculate statistics for each group.

3. Histograms: group by numeric ranges

Create bins for numeric data and calculate statistics for each range.

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)

Comparing building heights across time periods (Histograms)

Comparing neighborhood density (Aggregates)

Interactive visualization example

Here's how you might integrate these analysis methods with an interactive chart:

This example demonstrates how a user clicking on a pie chart slice could apply a filter to the map, highlighting only the buildings of that type. It also shows how you could fetch additional statistics based on the user's selection to enrich the visualization experience.

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 commented code in its entirety is shown below.

// 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 }
// 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 }
]
*/
// 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 }
]
*/
// 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 }
]
*/
// 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 }
]
*/
// Find average residential density across different neighborhoods
const downtownDensity = await felt.getAggregates({
    layerId: "buildings",
    boundary: [-122.335, 47.600, -122.330, 47.610], // downtown boundary
    filters: ["type", "eq", "residential"],
    aggregation: {
        method: "avg",
        attribute: "units_per_acre"
    }
});
// returns { avg: 124.7 }

const suburbanDensity = await felt.getAggregates({
    layerId: "buildings",
    boundary: [-122.200, 47.650, -122.150, 47.700], // suburban boundary
    filters: ["type", "eq", "residential"],
    aggregation: {
        method: "avg", 
        attribute: "units_per_acre"
    }
});
// returns { avg: 8.2 }
// Create a pie chart showing building type distribution
async function createBuildingTypePieChart() {
    // Get data for the chart
    const data = await felt.getCategoryData({
        layerId: "buildings",
        attribute: "type"
    });
    
    // Render pie chart (using a hypothetical chart library)
    const chart = renderPieChart(data, {
        valuePath: "count",
        labelPath: "value",
        onSliceClick: handleSliceClick
    });
    
    return chart;
}

// Handle user interaction with the chart
async function handleSliceClick(slice) {
    const buildingType = slice.label;
    
    // Apply filter to highlight this building type on the map
    await felt.setLayerFilters({
        layerId: "buildings",
        filters: ["type", "eq", buildingType],
        note: `Showing ${buildingType} buildings only`
    });
    
    // Get additional statistics for this building type
    const stats = await felt.getAggregates({
        layerId: "buildings",
        filters: ["type", "eq", buildingType],
        aggregation: {
            method: "avg",
            attribute: "year_built"
        }
    });
    
    // Update the UI with these statistics
    updateStatsPanel(`Average ${buildingType} building age: ${2025 - stats.avg}`);
}

// Initialize the chart when the page loads
createBuildingTypePieChart();
<!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 sample Felt SDK Application

Examples

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

Minimal visualization

{
  "version": "2.3",
  "type": "simple",
  "config": {},
  "paint": {},
  "label": {}
}

Point layer example

{
  "version": "2.3",
  "type": "simple",
  "config": { "labelAttribute": ["oper_cln", "owner_cln"] },
  "paint": {
    "color": "#8F7EBF",
    "strokeColor": "#CEC5E8"
  },
  "label": {
    "haloColor": "#E9E4F7",
    "color": "#8F7EBF"
  }
}

Polygon layer example

{
  "version": "2.3",
  "type": "simple",
  "config": { "labelAttribute": ["name"] },
  "label": {
    "minZoom": 4,
    "color": "#804779",
    "haloColor": "#EBD3E8",
    "haloWidth": 1,
    "fontSize": [12, 21]
  }
}

Line layer example

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

Color category

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

Numeric visualization

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

Heatmap visualization

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

H3 visualization

{
  "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"}
}
in our Help Center

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

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.

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

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.

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

Panel Elements

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

Text Elements

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

{
  type: "Text",
  content: "**Welcome!** This is a *formatted* text element with [links](https://felt.com).",
}

TextInput Elements

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

{
  type: "TextInput",
  placeholder: "First name",
  value: "",
  onChange: (args) => {
    console.log("New value:", args.value);
  },
}

Control Elements

Control elements allow users to choose from predefined options:

Available control elements include Select, CheckboxGroup, RadioGroup, and ToggleGroup. Each element supports similar properties:

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

Button Elements

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.

{
  type: "Button",
  label: "Submit",
  variant: "filled", // "transparent" | "outlined"
  tint: "primary", // "default" | "accent" | "danger"
  onClick: async () => {
    // Handle button click
  }
}

Button Rows

Group related buttons together to create clear action hierarchies:

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

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

Grid Elements

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

{
  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)" },
  ]
}

iframe Elements

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

{
  type: "Iframe",
  url: "https://example.com/dashboard",
  height: 400,
}

Divider Elements

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

{
  type: "Divider",
}

Panel State Management

Creating and Updating Panels

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

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" }]
  }
});

Targeted Panel Element Updates

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

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"]
});

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

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
Percent renter occupied housing by county
Average annual precipitation
Black Sea Grain Initiative

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
setTool
ElementsController
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

Upload map layer

post

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

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

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

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

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

Uploading the file to Amazon S3

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

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

You will receive a single set of URL and pre-signed params to upload the file. Only a single file may be uploaded — if you wish to upload several files at once, consider wrapping them in a zip file.

To upload the file using the pre-signed params, you must perform a multipart upload, and include the file contents in the file field.

# Requires requests library to be installed with: pip install requests
import requests

# Your API token should look like this:
# api_token = "felt_pat_ABCDEFUDQPAGGNBmX40YNhkCRvvLI3f8/BCwD/g8"
api_token = "<YOUR_API_TOKEN>"
map_id = "<YOUR_MAP_ID>"
path_to_file = "<YOUR_FILE_WITH_EXTENSION>" # Example: features.geojson

# Request  a pre-signed URL from Felt
layer_response = requests.post(
    f"https://felt.com/api/v2/maps/{map_id}/upload",
    headers={
        "authorization": f"Bearer {api_token}",
        "content-type": "application/json",
    },
    json={"name": "My new layer"},
)
presigned_upload = layer_response.json()
url = presigned_upload["data"]["attributes"]["url"]
presigned_attributes = presigned_upload["data"]["attributes"]["presigned_attributes"]
# A 204 response indicates that the upload was successful
with open(path_to_file, "rb") as file_obj:
    output = requests.post(
        url,
        # Order is important, file should come at the end
        files={**presigned_attributes, "file": file_obj},
    )
from felt_python import upload_file

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
POST /api/v2/maps/{map_id}/upload HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Accept: */*
Content-Length: 311

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

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
POST /api/v2/maps/{map_id}/add_source_layer HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Accept: */*
Content-Length: 56

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

Refresh map layer

post

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

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

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

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

  • For file uploads, the response of the initial POST request will include a URL and some presigned attributes, which will be used to upload the new file to Amazon S3. See #uploading-the-file-to-amazon-s3 for more details.

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
POST /api/v2/maps/{map_id}/layers/{layer_id}/refresh HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
{
  "layer_group_id": "luCHyMruTQ6ozGk3gPJfEB",
  "layer_id": "luCHyMruTQ6ozGk3gPJfEB",
  "presigned_attributes": {},
  "type": "upload_response",
  "url": "text"
}

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
POST /api/v2/maps/{map_id}/layers/{layer_id}/publish HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Accept: */*
Content-Length: 19

{
  "name": "My Layer"
}
{
  "caption": "text",
  "geometry_type": "Line",
  "hide_from_legend": true,
  "id": "luCHyMruTQ6ozGk3gPJfEB",
  "is_spreadsheet": true,
  "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
POST /api/v2/maps/{map_id}/layer_groups/{layer_group_id}/publish HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Accept: */*
Content-Length: 19

{
  "name": "My Layer"
}
{
  "caption": "text",
  "id": "luCHyMruTQ6ozGk3gPJfEB",
  "layers": [
    {
      "caption": "text",
      "geometry_type": "Line",
      "hide_from_legend": true,
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "is_spreadsheet": true,
      "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
GET /api/v2/library HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
{
  "layer_groups": [
    {
      "caption": "text",
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "layers": [
        {
          "caption": "text",
          "geometry_type": "Line",
          "hide_from_legend": true,
          "id": "luCHyMruTQ6ozGk3gPJfEB",
          "is_spreadsheet": true,
          "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"
}

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
GET /api/v2/maps/{map_id}/layers/{layer_id}/get_export_link HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
{
  "export_link": "text"
}

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

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
POST /api/v2/maps/{map_id}/layers/{layer_id}/custom_export HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Accept: */*
Content-Length: 63

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

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
GET /api/v2/user HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
{
  "email": "text",
  "id": "luCHyMruTQ6ozGk3gPJfEB",
  "name": "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
POST /api/v2/maps HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Accept: */*
Content-Length: 149

{
  "basemap": "text",
  "description": "text",
  "lat": 1,
  "layer_urls": [
    "text"
  ],
  "lon": 1,
  "public_access": "private",
  "title": "text",
  "workspace_id": "text",
  "zoom": 1
}
{
  "basemap": "text",
  "created_at": "2024-05-25T15:51:34",
  "element_groups": [
    {
      "elements": {
        "features": [
          {
            "geometry": {
              "felt:id": "luCHyMruTQ6ozGk3gPJfEB",
              "felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
            },
            "properties": {},
            "type": "Feature"
          }
        ],
        "type": "FeatureCollection"
      },
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "name": "text"
    }
  ],
  "elements": {
    "features": [
      {
        "geometry": {
          "felt:id": "luCHyMruTQ6ozGk3gPJfEB",
          "felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
        },
        "properties": {},
        "type": "Feature"
      }
    ],
    "type": "FeatureCollection"
  },
  "folder_id": "text",
  "id": "luCHyMruTQ6ozGk3gPJfEB",
  "layer_groups": [
    {
      "caption": "text",
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "layers": [
        {
          "caption": "text",
          "geometry_type": "Line",
          "hide_from_legend": true,
          "id": "luCHyMruTQ6ozGk3gPJfEB",
          "is_spreadsheet": true,
          "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"
}

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
POST /api/v2/maps/{map_id}/move HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Accept: */*
Content-Length: 39

{
  "project_id": "luCHyMruTQ6ozGk3gPJfEB"
}
{
  "basemap": "text",
  "created_at": "2024-05-25T15:51:34",
  "element_groups": [
    {
      "elements": {
        "features": [
          {
            "geometry": {
              "felt:id": "luCHyMruTQ6ozGk3gPJfEB",
              "felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
            },
            "properties": {},
            "type": "Feature"
          }
        ],
        "type": "FeatureCollection"
      },
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "name": "text"
    }
  ],
  "elements": {
    "features": [
      {
        "geometry": {
          "felt:id": "luCHyMruTQ6ozGk3gPJfEB",
          "felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
        },
        "properties": {},
        "type": "Feature"
      }
    ],
    "type": "FeatureCollection"
  },
  "folder_id": "text",
  "id": "luCHyMruTQ6ozGk3gPJfEB",
  "layer_groups": [
    {
      "caption": "text",
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "layers": [
        {
          "caption": "text",
          "geometry_type": "Line",
          "hide_from_legend": true,
          "id": "luCHyMruTQ6ozGk3gPJfEB",
          "is_spreadsheet": true,
          "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
POST /api/v2/maps/{map_id}/duplicate HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Accept: */*
Content-Length: 70

{
  "destination": {
    "project_id": "luCHyMruTQ6ozGk3gPJfEB"
  },
  "title": "text"
}
{
  "basemap": "text",
  "created_at": "2024-05-25T15:51:34",
  "element_groups": [
    {
      "elements": {
        "features": [
          {
            "geometry": {
              "felt:id": "luCHyMruTQ6ozGk3gPJfEB",
              "felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
            },
            "properties": {},
            "type": "Feature"
          }
        ],
        "type": "FeatureCollection"
      },
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "name": "text"
    }
  ],
  "elements": {
    "features": [
      {
        "geometry": {
          "felt:id": "luCHyMruTQ6ozGk3gPJfEB",
          "felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
        },
        "properties": {},
        "type": "Feature"
      }
    ],
    "type": "FeatureCollection"
  },
  "folder_id": "text",
  "id": "luCHyMruTQ6ozGk3gPJfEB",
  "layer_groups": [
    {
      "caption": "text",
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "layers": [
        {
          "caption": "text",
          "geometry_type": "Line",
          "hide_from_legend": true,
          "id": "luCHyMruTQ6ozGk3gPJfEB",
          "is_spreadsheet": true,
          "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
POST /api/v2/maps/{map_id}/update HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Accept: */*
Content-Length: 278

{
  "basemap": "text",
  "description": "text",
  "public_access": "private",
  "table_settings": {
    "default_table_layer_id": "luCHyMruTQ6ozGk3gPJfEB",
    "viewers_can_open_table": true
  },
  "title": "text",
  "viewer_permissions": {
    "can_duplicate_map": true,
    "can_export_data": true,
    "can_see_map_presence": true
  }
}
{
  "basemap": "text",
  "created_at": "2024-05-25T15:51:34",
  "element_groups": [
    {
      "elements": {
        "features": [
          {
            "geometry": {
              "felt:id": "luCHyMruTQ6ozGk3gPJfEB",
              "felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
            },
            "properties": {},
            "type": "Feature"
          }
        ],
        "type": "FeatureCollection"
      },
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "name": "text"
    }
  ],
  "elements": {
    "features": [
      {
        "geometry": {
          "felt:id": "luCHyMruTQ6ozGk3gPJfEB",
          "felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
        },
        "properties": {},
        "type": "Feature"
      }
    ],
    "type": "FeatureCollection"
  },
  "folder_id": "text",
  "id": "luCHyMruTQ6ozGk3gPJfEB",
  "layer_groups": [
    {
      "caption": "text",
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "layers": [
        {
          "caption": "text",
          "geometry_type": "Line",
          "hide_from_legend": true,
          "id": "luCHyMruTQ6ozGk3gPJfEB",
          "is_spreadsheet": true,
          "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
GET /api/v2/maps/{map_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
{
  "basemap": "text",
  "created_at": "2024-05-25T15:51:34",
  "element_groups": [
    {
      "elements": {
        "features": [
          {
            "geometry": {
              "felt:id": "luCHyMruTQ6ozGk3gPJfEB",
              "felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
            },
            "properties": {},
            "type": "Feature"
          }
        ],
        "type": "FeatureCollection"
      },
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "name": "text"
    }
  ],
  "elements": {
    "features": [
      {
        "geometry": {
          "felt:id": "luCHyMruTQ6ozGk3gPJfEB",
          "felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
        },
        "properties": {},
        "type": "Feature"
      }
    ],
    "type": "FeatureCollection"
  },
  "folder_id": "text",
  "id": "luCHyMruTQ6ozGk3gPJfEB",
  "layer_groups": [
    {
      "caption": "text",
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "layers": [
        {
          "caption": "text",
          "geometry_type": "Line",
          "hide_from_legend": true,
          "id": "luCHyMruTQ6ozGk3gPJfEB",
          "is_spreadsheet": true,
          "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
DELETE /api/v2/maps/{map_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*

No content

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

No content

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
GET /api/v2/maps/{map_id}/layer_groups/{layer_group_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
{
  "caption": "text",
  "id": "luCHyMruTQ6ozGk3gPJfEB",
  "layers": [
    {
      "caption": "text",
      "geometry_type": "Line",
      "hide_from_legend": true,
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "is_spreadsheet": true,
      "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
POST /api/v2/maps/{map_id}/layer_groups/{layer_group_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Accept: */*
Content-Length: 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
DELETE /api/v2/maps/{map_id}/layer_groups/{layer_group_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*

No content

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
GET /api/v2/maps/{map_id}/layers HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
[
  {
    "caption": "text",
    "geometry_type": "Line",
    "hide_from_legend": true,
    "id": "luCHyMruTQ6ozGk3gPJfEB",
    "is_spreadsheet": true,
    "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
POST /api/v2/maps/{map_id}/layers HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Accept: */*
Content-Length: 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
GET /api/v2/maps/{map_id}/layer_groups HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
[
  {
    "caption": "text",
    "id": "luCHyMruTQ6ozGk3gPJfEB",
    "layers": [
      {
        "caption": "text",
        "geometry_type": "Line",
        "hide_from_legend": true,
        "id": "luCHyMruTQ6ozGk3gPJfEB",
        "is_spreadsheet": true,
        "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
POST /api/v2/maps/{map_id}/layer_groups HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Accept: */*
Content-Length: 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
POST /api/v2/maps/{map_id}/layers/{layer_id}/update_style HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Accept: */*
Content-Length: 12

{
  "style": {}
}
{
  "caption": "text",
  "geometry_type": "Line",
  "hide_from_legend": true,
  "id": "luCHyMruTQ6ozGk3gPJfEB",
  "is_spreadsheet": true,
  "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"
}

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
POST /api/v2/duplicate_layers HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Accept: */*
Content-Length: 92

[
  {
    "destination_map_id": "luCHyMruTQ6ozGk3gPJfEB",
    "source_layer_id": "luCHyMruTQ6ozGk3gPJfEB"
  }
]
{
  "layer_groups": [
    {
      "caption": "text",
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "layers": [
        {
          "caption": "text",
          "geometry_type": "Line",
          "hide_from_legend": true,
          "id": "luCHyMruTQ6ozGk3gPJfEB",
          "is_spreadsheet": true,
          "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 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
GET /api/v2/maps/{map_id}/elements HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
{
  "features": [
    {
      "geometry": {
        "felt:id": "luCHyMruTQ6ozGk3gPJfEB",
        "felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
      },
      "properties": {},
      "type": "Feature"
    }
  ],
  "type": "FeatureCollection"
}

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
POST /api/v2/maps/{map_id}/elements HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Accept: */*
Content-Length: 165

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

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
DELETE /api/v2/maps/{map_id}/elements/{element_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*

No content

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
GET /api/v2/maps/{map_id}/element_groups/{group_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
{
  "features": [
    {
      "geometry": {
        "felt:id": "luCHyMruTQ6ozGk3gPJfEB",
        "felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
      },
      "properties": {},
      "type": "Feature"
    }
  ],
  "type": "FeatureCollection"
}

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
GET /api/v2/maps/{map_id}/element_groups HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
[
  {
    "color": "text",
    "elements": {
      "features": [
        {
          "geometry": {
            "felt:id": "luCHyMruTQ6ozGk3gPJfEB",
            "felt:parentId": "luCHyMruTQ6ozGk3gPJfEB"
          },
          "properties": {},
          "type": "Feature"
        }
      ],
      "type": "FeatureCollection"
    },
    "id": "text",
    "name": "text",
    "symbol": "text"
  }
]

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
POST /api/v2/maps/{map_id}/element_groups HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Accept: */*
Content-Length: 92

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

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
GET /api/v2/maps/{map_id}/comments/export HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
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
POST /api/v2/maps/{map_id}/comments/{comment_id}/resolve HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
{
  "comment_id": "luCHyMruTQ6ozGk3gPJfEB"
}

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
DELETE /api/v2/maps/{map_id}/comments/{comment_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*

No content

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
GET /api/v2/projects HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
[
  {
    "id": "luCHyMruTQ6ozGk3gPJfEB",
    "links": {
      "self": "https://felt.com/api/v2/projects/V0dnOMOuTd9B9BOsL9C0UjmqC"
    },
    "name": "text",
    "type": "project_reference",
    "visibility": "workspace"
  }
]

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
POST /api/v2/projects HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Accept: */*
Content-Length: 40

{
  "name": "text",
  "visibility": "workspace"
}
{
  "id": "luCHyMruTQ6ozGk3gPJfEB",
  "maps": [
    {
      "created_at": "2024-05-25T15:51:34",
      "folder_id": "text",
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "links": {
        "self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC"
      },
      "project_id": "text",
      "public_access": "private",
      "thumbnail_url": "text",
      "title": "text",
      "type": "map_reference",
      "url": "text",
      "visited_at": "text"
    }
  ],
  "name": "text",
  "type": "project",
  "visibility": "workspace"
}

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

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

{
  "name": "text",
  "visibility": "workspace"
}
{
  "id": "luCHyMruTQ6ozGk3gPJfEB",
  "maps": [
    {
      "created_at": "2024-05-25T15:51:34",
      "folder_id": "text",
      "id": "luCHyMruTQ6ozGk3gPJfEB",
      "links": {
        "self": "https://felt.com/api/v2/maps/V0dnOMOuTd9B9BOsL9C0UjmqC"
      },
      "project_id": "text",
      "public_access": "private",
      "thumbnail_url": "text",
      "title": "text",
      "type": "map_reference",
      "url": "text",
      "visited_at": "text"
    }
  ],
  "name": "text",
  "type": "project",
  "visibility": "workspace"
}

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
GET /api/v2/sources HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
[
  {
    "automatic_sync": "enabled",
    "connection_type": "abs_bucket",
    "created_at": 1,
    "id": "luCHyMruTQ6ozGk3gPJfEB",
    "last_synced_at": 1,
    "links": {
      "self": "https://felt.com/api/v2/sources/V0dnOMOuTd9B9BOsL9C0UjmqC"
    },
    "name": "text",
    "owner_id": "luCHyMruTQ6ozGk3gPJfEB",
    "permissions": {
      "type": "workspace_editors"
    },
    "sync_status": "syncing",
    "type": "source_reference",
    "updated_at": 1,
    "workspace_id": "luCHyMruTQ6ozGk3gPJfEB"
  }
]

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
POST /api/v2/sources HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
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"
}

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

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
POST /api/v2/sources/{source_id}/sync HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
{
  "automatic_sync": "enabled",
  "connection_type": "abs_bucket",
  "created_at": 1,
  "id": "luCHyMruTQ6ozGk3gPJfEB",
  "last_synced_at": 1,
  "links": {
    "self": "https://felt.com/api/v2/sources/V0dnOMOuTd9B9BOsL9C0UjmqC"
  },
  "name": "text",
  "owner_id": "luCHyMruTQ6ozGk3gPJfEB",
  "permissions": {
    "type": "workspace_editors"
  },
  "sync_status": "syncing",
  "type": "source_reference",
  "updated_at": 1,
  "workspace_id": "luCHyMruTQ6ozGk3gPJfEB"
}

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
GET /api/v2/sources/{source_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
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
DELETE /api/v2/sources/{source_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
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
POST /api/v2/sources/{source_id}/credentials HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
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
POST /api/v2/sources/{source_id}/credentials/{credential_id}/update HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
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
DELETE /api/v2/sources/{source_id}/credentials/{credential_id} HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
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

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
POST /api/v2/maps/{map_id}/embed_token HTTP/1.1
Host: felt.com
Authorization: Bearer YOUR_API_KEY
Accept: */*
{
  "expires_at": "2024-05-25T15:51:34",
  "token": "text"
}
<iframe src="https://felt.com/embed/map/{mapId}?token={token}"></iframe>