Supported API for Teams


let's begin

CalTopo has a supported API for Teams. Below you will find information on how the Team Admin can get the credential secret, credential id, and your team ids, how to sign requests, and some sample code .

Topics on this Page:

Getting Your Credentials

The CalTopo API is built around service accounts. You cannot log in to these accounts, but permissions and visibility into parent and child teams works the same as tradditional user accounts. For details on team membership levels, see this this guide.

All CalTopo accounts (team, individual and service) are identified by a 6-digit alphanumeric ID. Accounts also have credentials associated with them, allowing secure access to the API. While individual accounts may have multiple credentials, for example one for each device a user has signed in on, service accounts are currently limited to a single credential per account. These credentials consist of two parts, an ID included with each request to identify the credential being used, and a secret which is never sent to the server, but is used to sign requests. These are obtained when an admin creates a service account on the Team Admin Page. While the ID can be viewed later, the secret appears only once and must be copied and stored securely at the time of creation.

In the Team Admin page, click the Details tab and near the bottom you can create a Service Account. When you click Create a Service Account you can choose a title and a permission level, then click Create

When you click Create you will get a Credential Secret. This will be the only time you will see this, you need to copy it and keep it in a safe place. It should only be shared with people who need it to build any integration that will be using the API.

Back to Top


Creating a Signed Request

Description

In addition to other endpoint-specific parameters, all API requests include an expiration time (in epoch milliseconds), the ID of the credential used to sign the request, and an HMAC-SHA256 based signature for authentication. This script shows how to generate a signed request, and providing a basic building block for all API requests.

Python
Java

import base64
import hmac
import json
import time
import urllib.error
import urllib.request

from urllib.parse import urlencode

# Global variables
CREDENTIAL_ID = "your auth ID"
CREDENTIAL_SECRET = "your credential secret"
DEFAULT_TIMEOUT_MS = 2 * 60 * 1000

def sign(method, url, expires, payload_string, credential_secret):
    """
    Generates an HMAC signature for API request authentication.

    Parameters:
        method (str): HTTP method (e.g., "GET", "POST").
        url (str): API endpoint URL.
        expires (int): Expiration time in milliseconds.
        payload_string (str): JSON payload as a string.
        credential_secret (str): Credential Secret.

    Returns:
        str: Base64-encoded signature.
    """
    message = f"{method} {url}\n{expires}\n{payload_string}"
    secret = base64.b64decode(credential_secret)
    signature = hmac.new(
        secret, message.encode(), "sha256"
    ).digest()
    return base64.b64encode(signature).decode()

def caltopo_request(method, endpoint, credential_id, credential_secret, payload=None):
    """
    Prepares and sends an authenticated API request.

    Parameters:
        method (str): HTTP method (e.g., "GET", "POST").
        url (str): API endpoint URL.
        payload (dict): Data to send with the request.
        credential_id (str): Credential ID.
        credential_secret (str): Credential Secret .

    Returns:
        dict or None: API response as JSON, or None
        if an error occurs.
    """
    payload_string = json.dumps(payload) if payload else ""
    expires = int(time.time() * 1000) + DEFAULT_TIMEOUT_MS
    signature = sign(
        method, endpoint, expires, payload_string, credential_secret
    )
    parameters = {
        "id": credential_id,
        "expires": expires,
        "signature": signature,
    }
    if method.upper() == "POST" and payload is not None:
        parameters["json"] = payload_string
        query_string = ""
    else:
        query_string = f"?{urlencode(parameters)}"
    url = f"https://caltopo.com{endpoint}{query_string}"

    body = (
        urlencode(parameters).encode()
        if method.upper() == "POST" and payload
        else None
    )

    request = urllib.request.Request(
        url, data=body, method=method.upper()
    )
    request.add_header(
        "Content-Type", "application/x-www-form-urlencoded"
    )

    if body is not None:
        request.add_header("Content-Length", str(len(body)))

    with urllib.request.urlopen(request) as response:
        response_data = response.read().decode("utf-8")
        if response_data:
            return json.loads(response_data).get("result")



import org.json.JSONObject;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public class Main {

// Global variables.
public static final String CREDENTIAL_ID = "your credential ID";
public static final String CREDENTIAL_SECRET = "your credential secret";
public static final int DEFAULT_TIMEOUT_MS = 2 * 60 * 1000;

public static String sign(
        String method,
        String url,
        long expires,
        String payload,
        String credentialSecret
) {
    try {
        // Construct the message
        String message = method + " " + url + "\n"
                + expires + "\n"
                + (payload != null ? payload : "");

        // Decode the authentication key (Base64)
        byte[] secretKey = Base64.getDecoder().decode(credentialSecret);

        // Create a Mac instance with the HMAC-SHA256 algorithm
        Mac hmac = Mac.getInstance("HmacSHA256");
        SecretKeySpec keySpec = new SecretKeySpec(secretKey, "HmacSHA256");
        hmac.init(keySpec);

        // Generate the signature
        byte[] signatureBytes = hmac.doFinal(message.getBytes(StandardCharsets.UTF_8));

        // Return the signature as a Base64-encoded string
        return Base64.getEncoder().encodeToString(signatureBytes);

    } catch (Exception e) {
        throw new RuntimeException("Error while generating HMAC signature", e);
    }
}

public static String encodeParams(Map params) {
    String paramString = "";
    for (Map.Entry entry : params.entrySet()) {
        paramString += entry.getKey() + "=" + URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8) + "&";
    }
    return paramString.substring(0, paramString.length() - 1);
}

public static JSONObject caltopoRequest(
        String method,
        String url,
        String payload,
        String credentialId,
        String credentialSecret
) {
    try {
        method = method.toUpperCase();
        long expires = System.currentTimeMillis() + DEFAULT_TIMEOUT_MS;

        // Generate the signature
        String signature = sign(method, url, expires, payload, credentialSecret);

        // Construct the query string
        String query = "";
        Map params = new HashMap<>();
        params.put("id", credentialId);
        params.put("expires", String.valueOf(expires));
        params.put("signature", signature);
        if (method.equals("POST") && payload != null) {
            params.put("json", payload);
        } else {
            query = "?" + encodeParams(params);
        }

        // Construct the full URL
        String fullUrl = "https://caltopo.com" + url + query;

        // Open a connection
        HttpURLConnection connection = (HttpURLConnection) new URL(fullUrl).openConnection();
        connection.setRequestMethod(method);

        if (method.equals("POST") && payload != null) {
            connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
            connection.setDoOutput(true);
            OutputStream os = connection.getOutputStream();
            DataOutputStream dos = new DataOutputStream(os);
            dos.writeBytes(encodeParams(params));
            dos.flush();
            dos.close();
            os.close();
        }

        // Get the response code
        int responseCode = connection.getResponseCode();
        BufferedReader reader;

        // Handle the response stream
        if (responseCode == HttpURLConnection.HTTP_OK) {
            reader = new BufferedReader(new InputStreamReader(connection.getInputStream(),
                    StandardCharsets.UTF_8));
        } else {
            reader = new BufferedReader(new InputStreamReader(connection.getErrorStream(),
                    StandardCharsets.UTF_8));
        }

        // Read the response
        StringBuilder response = new StringBuilder();
        String line;
        while ((line = reader.readLine()) != null) {
            response.append(line);
        }
        reader.close();

        // Return the response if HTTP_OK, otherwise throw an error
        if (responseCode == HttpURLConnection.HTTP_OK) {
            JSONObject responseJson = new JSONObject(response.toString());
            return responseJson.getJSONObject("result");
        } else {
            throw new RuntimeException("Request failed with code " + responseCode
                    + ": " + response);
        }

    } catch (Exception e) {
        throw new RuntimeException("Error during CalTopo request", e);
    }
}


Back to Top


Get Account Data

Endpoint

GET https://caltopo.com/api/v1/acct/{team_id}/since/{timestamp}

Description

CalTopo's data model is broadly divided between account objects like maps and PDFs, and map objects like markers and shapes that live on a specific map. This sections shows you how to retrieve a list of all account objects, across all subteams a team has visibility into. The API will only return objects created or changed since the provided timestamp; if you are making multiple requests to this endpoint per minute, please use a recent timestamp value for lighter weight, more performant requests. The Service Account must have at least ADMIN permission.

Request Parameters
Parameter Type Description
team_id string The ID of the team to query.
timestamp integer A Unix timestamp in milliseconds. Use 0 to retrieve all current data, or provide a timestamp to get changes since that time. Use the response timestamp in future requests to fetch updates since the last request.

You can use the caltopo_request function provided above to access this endpoint as shown below.

Python

team_id = "your team ID"

ts = 0  # Replace with a Unix timestamp in milliseconds if needed
team_data = caltopo_request(
    "GET",
    f"/api/v1/acct/{team_id}/since/{ts}",
    CREDENTIAL_ID,
    CREDENTIAL_SECRET,
)



Team Data

The server's response is a wrapped GeoJSON FeatureCollection, with each account object appearing as an entry in the features array. If the server is not sure whether or not an account object has been deleted since the provided timestamp, it will also include an ids map enumerating all account object IDs, so that clients can determine if the account no longer has access to a given object. The response will additionally include a groups array with an entry for each subteam the service account has visibility into, an accounts array that duplicates this list with the service account added, and a rels array representing both subteams and bookmarked maps; these entries are used by the CalTopo mobile app but should be ignored for API access purposes.

Click to expand

As one example use case, the Python code below shows how to iterate through the response and develop a list of all maps the team has access to.

Python

# Extract a list of map IDs, map titles, and the teams that own them
collaborative_maps = {
    feature["id"]: feature
    for feature in team_data["features"]
    if feature["properties"].get("class") == "CollaborativeMap"
} 

Back to Top


Back Up the Maps in a Team's Account

After getting the data from the Team and parsing it to get the colabrative map info you can then retrieve that data and save it to a file.

Python


for map_id, map_info in collaborative_maps.items():
    map_data = caltopo_request(
        "GET",
        f"/api/v1/map/{map_id}/since/0", CREDENTIAL_ID, CREDENTIAL_SECRET
    )

    title = map_info['properties']['title']
    team_id = map_info['properties']['accountId']

    with open(
        f"{map_id}_{title}_{team_id}.json", "w") as fp:
        json.dump(map_data["state"], fp)


Back to Top


Create a Map in a Team Account

Endpoint

POST https://caltopo.com/api/v1/acct/{team_id}/CollaborativeMap

Description

This endpoint creates a new Collaborative Map and adds features such as a markers, lines, and polygons to it. The input to this endpoint is a single JSON `Map` object that includes the necessary properties and features. The Service Account must have at least UPDATE permission.

Request Parameters
Parameter Type Description
team_id string The ID of the team you want the map in.
Acquiring the Team ID

To create a map and save it to a team or sub-team, first obtain the team’s ID. One way is to check the URL on the team's admin page:

https://caltopo.com/group/{team_id}/admin/details

Alternatively, extract the team ID from the account data using the sample Python code below:

Python

teams = [[team["id"], team["properties"].get("title")] 
               for team in team_data.get("accounts", []) 
               if team.get("properties", {}).get("title")]

Use the team's ID with the credential ID and secret to create or update a map. The team ID is only needed to obtain the team ID.

Map Object Structure

The "Map" object defines the map's properties, sharing settings, and a collection of features to be added. Below is the structure of the "Map" object:

Field Type Description
properties object

Defines the map's:

  • Title: The map's title, e.g., "Map Title Example".
  • Mode: Specifies the mode for the map:
    • "sar" — Search and Rescue mode
    • "cal" — Recreational mode
  • Configuration: Defines the map layers, e.g:
    • "mbt" — MapBuilder Topo layer
    • "mbh" — MapBuilder Hybrid layer
    • "imagery" — Global Imagery layer
  • Sharing settings: Determines sharing permissions for the map:
    • "PRIVATE" — Map is only accessible to the creator.
    • "SECRET" — Map is accessible via a secret URL.
    • "URL" — Map is accessible via a public URL.
    • "PUBLIC" — Map is publicly accessible by anyone.
state object Contains the FeatureCollection of map features such as markers, lines, and polygons. The format follows the standard GeoJSON format.
Features in the state Object

The state object is a GeoJSON `FeatureCollection` that contains the following features:

  • Point: Defines a marker with specific coordinates, title, and description.
  • LineString: Defines a line with an array of coordinates and styling properties.
  • Polygon: Defines a polygon with an array of coordinates, stroke, fill, and opacity properties.

Other feature types are not supported and may result in unexpected or undesired behavior.

Request Body Example
Click to expand

To match the base layers and overlays of another map in the team, retrieve its activeLayers using the code below.

Python

map_id = "your map id"

active_layers = json.loads(
    collaborative_maps[map_id]['properties']['config']
    )['activeLayers']


Example Usage

To create a map, use the `sign` and `caltopo_request` along with the code below.

Python


team_id = "your team ID"

caltopo_request(
    "POST",
    f"/api/v1/acct/{team_id}/CollaborativeMap",
    CREDENTIAL_ID,
    CREDENTIAL_SECRET,
    json.loads(map_json)
)


Back to Top


Delete a Map in a Team Account

Deleting a map is straightforward using the CalTopo API. You need the map ID and the team ID of the map you want to delete.

Endpoint

DELETE https://caltopo.com/api/v1/acct/{team_id}/CollaborativeMap/{map_id}

Description

This sends a DELETE request, the Service Account must have at least MANAGE permission.

Request Parameters
Parameter Type Description
map_id string The ID of the map to query.
team_id string The ID of the team the map is in.
Example Usage

The example below demonstrates how to delete a map.

Python


team_id = "your team ID"
map_id = "your map ID"

caltopo_request(
    "DELETE",
    f"/api/v1/acct/{team_id}/CollaborativeMap/{map_id}",
    CREDENTIAL_ID,
    CREDENTIAL_SECRET,
)


Back to Top


Retrieve Map Data

Endpoint

GET https://caltopo.com/api/v1/map/{map_id}/since/{timestamp}

Description

This section shows how to retrieve objects belonging to a particular map. As with the account endpoint above, a timestamp allows incremental fetches of only updates since the given time, and should be used when the endpoint will be subjected to frequent queries.

The Service Account must have at least READ access to view features and metadata.

Request Parameters
Parameter Type Description
map_id string The ID of the map to query.
timestamp integer A Unix timestamp in milliseconds. Use 0 to retrieve all current data. Responses include a server timestamp that can be used on a subsequent request to only obtain incremental changes.

You can use the caltopo_request function provided above to access this endpoint as shown below.

Python


map_id = "your map id"

ts = 0  # Replace with a Unix timestamp in milliseconds if needed
map_data = caltopo_request(
    "GET",
    f"/api/v1/map/{map_id}/since/{ts}",
    CREDENTIAL_ID,
    CREDENTIAL_SECRET,
)


As with the account object endpoint, the server's response is a wrapped GeoJSON feature collection, and if the server is not sure whether a map object has been deleted since the provided timestamp, a map of object IDs so that the client can identify any objects it has which have been deleted on the server.

Back to Top


Managing Map Objects

Once a map is created, any edits to its contents are done individually to each object; there is no way to send a batch of changes to a single endpoint.

Endpoints

Add a Map Object
POST https://caltopo.com/api/v1/map/{map_id}/{object_type}

Edit a Map Object
POST https://caltopo.com/api/v1/map/{map_id}/{object_type}/{object_ID}

Delete a Map Object
DELETE https://caltopo.com/api/v1/map/{map_id}/{object_type}/{object_ID}

Description

This endpoints allow you to edit or add a new object to an existing map. To add a new object the request should include a JSON object with the object's name, coordinates, and properties.

To edit an object the request should include a JSON object with the object's name, object Id and properties.

The Service Account must have at least UPDATE permission to use this endpoint.

Request Parameters
Parameter Type Description
map_id string The ID of the map to which the marker is being modified.
object_type string The type of map object to modify, e.g.:
  • Shape – For lines and polygons.
  • Marker – For markers
object_Id string The ID of the object you want to modify.

Acquiring the Map ID and the Object ID

Editing or adding to a map is similar to creating one, with the key difference being the need for the map ID. Find the map ID by opening the map in your browser and checking the URL.

https://caltopo.com/m/{map_id}

Alternatively, retrieve the map ID from the account data, as demonstrated earlier when extracting a list of map IDs and titles from the data.

The object id can be acquired from the map data we got when we retrieved the map data above.

Request Body Examples
Click to expand
Example Usage

To use the example below, ensure that you include the sign and caltopo_request functions in your code. This example demonstrates how to add a marker to a map.

Python


map_id = "your map id"

caltopo_request(
    "POST",
    f"/api/v1/map/{map_id}/Marker",
    CREDENTIAL_ID,
    CREDENTIAL_SECRET,
    json.loads(add_marker_data)
)


Back to Top


Import a Photo Into a Map

The code used to import a photo into a map below relies on the sign and caltopo_request function. The Service Account must have at least UPDATE permission

Python

import uuid

map_id = "map_id"
team_id = "team_id"
photo_path = "photo_path"

"""Create backend media object."""
media_id = str(uuid.uuid4())
media_metadata_payload = {
    "properties": {
        "creator": team_id
    }
}
caltopo_request(
    "POST", f"/api/v1/media/{media_id}", 
    CREDENTIAL_ID, 
    CREDENTIAL_SECRET,
    media_metadata_payload 
)

"""Upload media data."""
with open(photo_path, "rb") as img_file:
    base64_data = base64.b64encode(img_file.read()).decode()

media_data_payload = {"data": base64_data}
caltopo_request(
    "POST", f"/api/v1/media/{media_id}/data", 
    CREDENTIAL_ID, 
    CREDENTIAL_SECRET,
    media_data_payload
)

"""Attach media object to map."""
media_object_payload = {
    "type": "Feature",
    "id": None,
    "geometry": {
        "type": "Point",
        "coordinates": [-119, 39]
    },
    "properties": {
        "parentId": "",
        "backendMediaId": media_id,
        "title": "Photo Title",
        "heading": None,
        "description": "",
        "marker-symbol": "aperture",
        "marker-color": "#FF0000",
        "marker-size": 1,
    }
}

caltopo_request(
    "POST", f"/api/v1/map/{map_id}/MapMediaObject", 
    CREDENTIAL_ID, 
    CREDENTIAL_SECRET,
    media_object_payload
)


Back to Top