NAV

Introduction

Map Buddy is a platform for creating custom, interactive maps. These maps are primarily consumed by the Map Buddy Mobile App, the Map Buddy Web App, or by embedding maps on third party websites. Entries that are added to maps are called posts and can represent permanent things, like buildings, or timely things, like events. Posts and maps are highly configurable and you can decide who gets read and write access to which maps, which map posts can be replied to, etc.

This page contains documentation for the Map Buddy HTTP API. By using this API, you are able to perform nearly all of the functionality that the Map Buddy app can perform, but in a programmatic manner. This includes things like making new maps, adding posts to the maps, replying to posts, subscribing to maps, etc. Using the API you can even do things that the apps itself don't yet support, such as modifying maps, updating posts, and adding advanced metadata.

Entity Relationships

There are dozens of endpoints available in the Map Buddy API. These endpoints modify several different entities and their relationships. Here's a high level diagram showing the relationships between these entities:

Map Buddy Entity Relationships

Postman Collection

To make it simpler to follow along, we have a Postman collection available. This collection can be imported into the Postman app on your computer to make it easier to get started with the API.

The environment file provides three variables. The first is {{server}} and is always set to https://api.mapbuddy.app. The second is {{username}} and is the username field of basic HTTP auth. Despite the name it should be set to your user_id. The third variable is {{password}} and is the password field of basic HTTP auth. It should be set to your auth_token. Both of these values are returned by the Confirm Auth Code endpoint.

Reporting Bugs

If you find any bugs in the API, particularly any security issues, please contact us with a description of the bug and we'll make it a priority to fix it.

Requests

As the Map Buddy API uses the HTTP protocol, communication is performed by way of clients sending requests to the server, the server processing the request, and the server returning a response to the client.

Map Buddy uses JSON to represent objects, both in the requests (when necessary), and in all of the responses (except for notable scenarios).

In situations where a client makes a PUT or POST HTTP request including a request body, it's recommended that the client sets the Content-Type: application/json header to signal that the body is JSON. It's also recommended that the client always sets the Accept: application/json, which tells the server that the client wants JSON back.

API versioning is performed as part of the URL. For example, the current version of the API is v2, and all endpoint paths begin with /v2/. Backwards-compatible changes will be made against the current version of the API. Breaking changes will be added to endpoints with a higher version number.

The hostname of the API is https://api.mapbuddy.app and all of the request paths used in this API should be appended to that hostname. Here's an example full URL for a request:

https://api.mapbuddy.app/v2/maps/global/posts?lat=37.772336&lon=-122.390819

JSON Data Conventions

Here are some of the conventions that the Map Buddy API uses when it comes to JSON objects.

Dates

2021-08-17T00:20:20.386Z

Dates are formatted using the ISO 8601 format. This is an example of a date returned from Map Buddy.

Notice that the date does include a timezone but that the timezone is always UTC. Whatever software you use to parse dates should be able to then display that date in your local timezome without ambiguity of the absolute time that the date represents. Note that there is no indication as to the local timezone that the event occurred in.

Geolocation Constraints

Latitude values are allowed to be within the range of -180 to 180 degrees, inclusive.

Longitude values need to be between -85 and 85 degrees, inclusive.

Latitude values are shorted to lat and longitude values are shortened to lon. This convention is used everywhere except for in some of the human-readable reports.

Empty Values

In some situations, an empty value will be represented as a field with a value of null. In other situations, the field may be omitted entirely. The client should treat these situations as equivalent.

When it comes to fields which are objects that may contain key/value pairs, an "empty" version can either be a null, an omitted key, or even an object with no keys within it (aka {}). Again, the client should treat these situations equivalently.

HTTP Status Codes

Here is a high level overview of the HTTP status codes returned by the Map Buddy API.

Status Code Meaning
200 The request was successful.
201 The request successfully created a resource.
400 There was an error with the client request. Trying again will probably also fail.
401 The authentication is invalid. You may need to login again.
403 The request is correct but permissions are wrong.
404 A referenced resource does not exist.
500 There was a server error. You may try again.
501 This API endpoint is not yet supported.

Error Payloads

{
  "error": {
    "code": "machine_keyable_error_message",
    "message": "Human-readable error message"
  }
}

When a response contains a 4XX or 5XX status code, the JSON response body will conform to the given shape.

The error.code property should remain consistent and is appropriate for performing client-side logic. For example, if (error.code == 'not_found'). The error.message property may change at any time, like clarifying an error or fixing a typo, and is a string that may be shown to a user. This field isn't appropriate to do code comparisons with.

If you discover any error responses that don't follow this document shape then please report it to us as this is a server bug.

Here is a list of the most commonly encountered error codes:

Error Code Meaning
internal_server_error The server had an error.
invalid_auth Authentication failed, you will need to log in again.
invalid_lat_lon The provided geolocation falls outside of the acceptable range.
invalid_permissions The request was understood but the user doesn't have permission.
not_found The requested resource was not found. There are more specific not found errors, too.
unsupported The requested endpoint is not yet supported.
must_be_admin The request requires a map admin membership level.
must_be_super_admin The request requires super admin status.
must_be_authenticated The request requires authentication.

Each endpoint has additional, more specific errors that it may respond with.

Authentication

User accounts on Map Buddy don't have a password. Instead, users only provide their email address. Map Buddy then emails an auth code to the supplied email address. The auth code is then passed back to Map Buddy by the user to confirm they own the supplied email address.

An auth code is eight characters long, containing numbers and unambiguous capital letters. An example of an auth code is 2KPASHNG. This auth code is supplied to Map Buddy exactly once and then is no longer useful. When a user opens the email with the auth code, they may either copy and paste the code into the app or web browser, or they may click on a link. The link itself also uses the auth code and is merely a convenience.

The API endpoint to generate and email the auth code is documented in Generate Auth Email. The API endpoint to trade the auth code for an auth header is documented in Confirm Auth Code.

Currently, all authentication works using HTTP Basic auth. There is no support for OAuth or other authentication methods at this time. There is no three-legged auth support, either. Due to this, it's currently recommended that you build applications using Map Buddy that run on behalf of a Map Buddy user account that you own, not for other user's accounts.

Generate Auth Email

Request

POST /v2/auth/emailtoken
Content-Type: application/json
Accept: application/json
{
  "email": "username@example.com",
  "lat": 43.611,
  "lon": -84.248
}

Successful Response

{
  "ok": true
}

This is the first step of the authentication process. The user provides their email address and a link to sign in is then emailed to them, along with an auth code that the user can copy and paste. The flow is exactly the same for a user creating a new account, and a returning user.

If you're building an application of your own and you want to get an auth header for your app then you don't want to click the link in the email to login. Instead, copy the auth code from the email and use it for the next request, Confirm Auth Code.

The user's current physical location should be provided as the lat and lon attributes in the request body. Their location is used for automatically joining maps based on geographical boundaries. In the context of a browser this means you'll need to prompt the user for their location before allowing them to authenticate.

Confirm Auth Code

Request

POST /v2/auth/confirm
Content-Type: application/json
Accept: application/json
{
  "code": "AUTH_CODE_FROM_EMAIL",
  "lat": 43.611,
  "lon": -84.248
}

Successful Response

{
  "user_id": "dc1caf1b-f7d5-47c2-bfb9-88329f3bbe19",

  "auth_token": "yEOeGMZEDMWA",
  "authorization_raw": "dc1caf1b-f7d5-47c2-bfb9-88329f3bbe19:yEOeGMZEDMWA",
  "authorization_header": "Basic ZGMxY2FmMWItZjdkNS00N2MyLWJmYjktODgzMjlmM2JiZTE5OnlFT2VHTVpFRE1XQQo=",

  "admin": false,
  "admin_panel": false,

  "username": "tlhunter",
  "using_default_username": false
}

As you can see there are several fields in this response payload. The first is the user_id field. This is the unique identifier of the user who just logged in. It represents a primary key of the user account. While it's possible for the username to change, the user ID will never change. This value is required by some other endpoints and should be stored by the client.

The next field is auth_token. This is essentially the HTTP "password" that future requests will use. An auth token is 12 characters long. Another related field is authorization_raw, which is the user_id and auth_token fields concatenated together with a colon (:) character. The client can safely ignore the auth_token and authorization_raw fields.

The most important field in this response is authorization_header. This is the value of authorization_raw, having been encoded using the base64 algorithm, and prefixed with the string Basic. In other words, Map Buddy did all the work for you to generate the necessary Authorization header needed for future requests.

If a user goes through this process multiple times they will generate multiple auth tokens / auth headers. This is OK as old auth headers will continue to work just fine. This allows a single user to login using multiple devices or even third party apps like you may be developing.

Do keep in mind that auth headers eventually expire. The time it takes to expire will be between a month and a year, subject to change. If a security incident were to happen then Map Buddy might invalidate all active auth headers. When an auth header does expire then API requests making use of it will get errors with an HTTP status code of 401 and an error code of invalid_auth.

Post Level Endpoints

Every post has a level field associated with it. This field represents the "loudness" of a post. Currently, the list of levels is mostly static and global, which is to say the values don change often and affect all maps. For that reason, this data should be requested once per user session and then cached in memory, assuming your application depends on it.

The Map Buddy mobile app uses the data from this endpoint to display a slider to users when they create a new post. It allows the user to make an informed decision of what level to choose when creating a post. If your are building an app that doesn't allow a user to create posts then you might not need this data.

Describe Post Levels

Request

GET /v2/levels
Accept: application/json

Successful Response

{
  "1": {
    "distance": 10,     "label": "2yr",
    "expire": 63072000, "expire_human": "2 Years",
    "name": "whisper",  "name_short": "wsp"
  },
  "2": {
    "distance": 50,     "label": "6mo",
    "expire": 15552000, "expire_human": "6 Months",
    "name": "mutter",   "name_short": "mtr"
  },
  "3": {
    "distance": 100,    "label": "1mo",
    "expire": 2592000,  "expire_human": "1 Month",
    "name": "talk",     "name_short": "tlk"
  },
  "4": {
    "distance": 500,    "label": "1wk",
    "expire": 604800,   "expire_human": "1 Week",
    "name": "shout",    "name_short": "sht"
  },
  "5": {
    "distance": 10000,  "label": "1hr",
    "expire": 3600,     "expire_human": "1 Hour",
    "name": "scream",   "name_short": "scm"
  },
  "beacon": {
    "distance": null,   "label": "1yr",
    "expire": 31536000, "expire_human": "1 Year",
    "name": "beacon",   "name_short": "bcn"
  },
  "ids": [ 1, 2, 3, 4, 5, "beacon" ]
}

This list may eventually be configured on a per-map basis. For example, a map for garage sales might only have a single level which is visible for 100km and expires in one day, and a map on national monuments might only need beacons. Bur for the foreseeable future this data globally affects all maps. If this change happens then it will result in a new version of the API being released.

Here is an explanation of the fields in the response payload:

Field Name Purpose
distance The visibility radius of a post, measured in meters. A null means always visible.
label A shorthand version of the expiration time.
expire The expiration time measured in seconds.
expire_human A human-readable version of the expiration time.
name A human-readable name for the level.
name_short A shorthand, three character name for the level.

The top-level ids field is an array of the levels and the order in which they should be presented to the user. This seemingly redundant information is included since the JSON spec doesn't technically guarantee order of property keys.

User Endpoints

These endpoints deal with user accounts, such as modifying and retrieving a particular user.

The avatar field represents a URL to an image that represents the user. Whenever displaying information about a user, this image should be included. Currently, the image can only be set when the user creates an account while authenticating via Google. Otherwise, users can upload an image that is associated with the email address they use on Map Buddy using the Gravatar service.

Change Username

Request

PUT /v2/users/{user_id}
Content-Type: application/json
Authorization: Basic HASH
Accept: application/json
{
  "username": "tlhunter"
}

Successful Response

{
  "id": "dc1caf1b-f7d5-47c2-bfb9-88329f3bbe19",
  "username": "tlhunter",
  "email": "hello@mapbuddy.app",
  "avatar": "https://s.gravatar.com/avatar/c0e344ddcbabb383f94b1bd3486e55ba?s=512&r=pg",
  "created": "2021-06-05T01:34:37.378Z"
}

Error Response

{
  "error": {
    "code": "registration_collision",
    "message": "Username or email taken"
  }
}

This endpoint can only modify the currently authenticated user. A user should only ever change their username once when they first create an account. Changing a username will not retroactively update map posts. Note that the User ID is redundantly used both in the Authorization header as well as in the URL.

Usernames must be unique across Map Buddy. For that reason, if a user attempts to change their username to one that is already claimed, they will receive an error code of registration_collision with an HTTP status code of 400.

Get User

Request

GET /v2/users/{user_id}
GET /v2/users/{username}?byusername
Accept: application/json

Successful Response

{
  "id": "dc1caf1b-f7d5-47c2-bfb9-88329f3bbe19",
  "username": "tlhunter",
  "email": "hello@mapbuddy.app",
  "created": "2016-08-24T07:03:43.444Z",
  "avatar": "https://s.gravatar.com/avatar/e79802a826a113cdb9c49f5d04dc6946.png?s=512&r=pg",
  "admin": false,
  "admin_panel": false
}

This endpoint retrieves information about a single user, using their User ID as a lookup. The email field is only returned if requesting data about the currently authenticated user. The field is omitted when looking up a different user.

If the ?byusername query parameter is present, then the URL segment performs a lookup using the username instead of the User ID.

Note that the admin field here represents the user's "Super Admin" status. It does not mean that a user is necessarily an admin of a particular map.

The admin_panel field dictates if the user has access to the administration tool located at admin.mapbuddy.app. This tool is available to non-Super Admin users, such as paying customers. Thus, being an admin is a bigger deal than having admin_panel access. For the most part, these two fields can be completely ignored.

Delete User

Request

DELETE /v2/users/{user_id}
Authorization: Basic HASH
Accept: application/json

Successful Response

{
  "id": "dc1caf1b-f7d5-47c2-bfb9-88329f3bbe19",
  "username": "tlhunter",
  "email": "hello@mapbuddy.app",
  "created": "2016-08-24T07:03:43.444Z",
  "avatar": "https://s.gravatar.com/avatar/e79802a826a113cdb9c49f5d04dc6946.png?s=512&r=pg"
}

This endpoint deletes the user account as specified in the URL. It can only delete the currently authenticated user. Attempting to delete another user will result in an error.

Once deleted, the user's account is gone entirely. The following data will be removed and cannot later be restored:

The following data is not deleted:

Maps aren't deleted because they aren't owned by any one user but instead can have multiple owners. If the user wants any maps deleted first then the Delete Map endpoint will need to be called first.

Map Endpoints

A map is something that users and organizations can create in order in order to add posts to. Posts added to a map usually adhere to some kind of theme. For example, the #garage-sales map contains posts that represent a garage sale event, while #foodtrucks contains posts representing a parked food truck.

Here is a map of SFPD incidents.

A map can be viewed by a user in a few ways. If the user clicks a link to a map on a mobile device, the map will open up either in the Map Buddy mobile app if it's installed, otherwise it will open in their browser. If a user clicks a link on desktop then it will always open in their browser. Maps can also be embedded in a website.

Users can also search for maps and subscribe to them using the mobile app. Once a map has been subscribed to, content from that map and other subscribed maps are combined into a single view called the user's home screen. Some map configuration options only apply when viewing a particular map (such as a default origin location or a website link). Otherwise, such configurations do not apply when the user is on their home screen. It wouldn't make sense to apply an origin for one map and an origin for another map at the same time.

Maps are uniquely identified by a name. In the app, these names are visually prefixed with a # symbol, and should be prefixed with the same symbol if you display the map name to a user. Map names can be up to 32 characters long, and can contain lowercase letters, numbers, and hyphens.

There are two special, "Virtual" maps that behave a little differently. These maps cannot be subscribed to and cannot be searched for. Within the Map Buddy app, they also can't be shared. The first map is named #home, and represents the home screen for a logged-in user. An anonymous user cannot view that map. The second map is named #suggested, and contains posts from default recommended maps based on the location of the request. Both of these maps are composites of other maps.

There's another map named #global, which all users automatically join when creating an account, regardless of their physical location.

Map Fields

Here's a list of field that are usually attached to maps:

Field Type Description
id String UUID representing the map
name String The unique name of the map
created Date Timestamp that the map was created
non_member_read Boolean Whether non-member users can read posts in the map
non_member_write Boolean Whether non-member users can write posts to the map
member_write Boolean If false, only admins can write posts to the map
virtual Boolean If this is a special "Virtual" map
listable Boolean If true, then it can be searched for and opened by non members
description String The map description, which is used for searching and displayed in the sidebar in the app
capabilities Object Either null or an object. This is described in more detail below
origin Object Either null or an object. This is described in more detail below
is_member Boolean Whether the user making the request is a member
is_admin Boolean Whether the user making the request is an administrator
has_password Boolean Whether the map has a password set or not
website String Either null or a string representing a URL related to the map
post_sort String Deprecated Used for client-side sorting
icon String A single emoji used as an icon to represent the map, and for new posts
member_count Integer The number of users subscribed to the map

Note that the is_admin and is_member fields are only available when getting a single map. When performing list requests these fields are omitted. The member_count field is omitted on some of the maps.

The origin field, if present, represents a default map position for the map. If present, it's an object with at least a lat and lon field, which are two numbers represent the geolocation that the map should be centered on when displaying to a user. There's a third optional field named zoom which is an Integer between 9 and 18, inclusive, representing a Google Maps zoom level.

Post Sorting

The post_sort column instructs the client to sort posts using a specific criteria. However, now that the Map Buddy mobile app no longer lists posts below the map, the field is considered deprecated. Here is the list of possible values:

Post Sort Description
null Posts are to be sorted by the order returned by the API
'distance' Posts are to be sorted by distance from the user
'alphabetical' Posts are to be sorted alphabetically by the message field
'age-oldest' Posts are to be sorted in descending order by the created field
'age-newest' Posts are to be sorted in ascending order by the created field
'expire-soonest' Posts are to be sorted in descending order by the expires field
'expire-latest' Posts are to be sorted in ascending order by the expires field

For now, if you display posts in a linear fashion, the client should sort posts using the above criteria.

Map Capabilities

The capabilities field is optional. If omitted, assume a default value of {}. Capabilities define what additional features are available within the map. These features mostly affect what metadata may be attached to posts within the map. Here are the capabilities:

Capability Type Status Description
permanent Boolean Free Allows map posts to have permanent flag set, preventing expiration
beacon Boolean Free Allows the creation of Beacon level posts, which are visible regardless of distance
nocluster Boolean Free Disables marker clustering when viewing map in client
image Boolean Paid Allows attaching images to posts (currently unsupported)
address Boolean Paid Allows attaching street addresses to posts for driving directions
social Boolean Paid Allows attaching social media URLs to posts
website Boolean Paid Allows attaching a website URL to posts
checkin String Paid Allows posts that can be checked-in to if value present. Value should be a URL to a terms of service page for the map
markdown Boolean Paid Allows posts to have an additional markdown field, which allows longer content (displayed below regular message in a smaller font)
contact Boolean Paid Allows attaching email address and phone number to posts

List Maps

Request

GET /v2/maps?search=search%20terms
Accept: application/json

Successful Response

[
  {
    "id": "22d36af1-b3b9-4d82-977d-6b821c678f87",
    "name": "global",
    "created": "2016-08-25T04:45:01.713Z",
    "non_member_read": true,
    "non_member_write": true,
    "member_write": true,
    "description": "Where the fun happens",
    "website": "https://example.org",
    "listable": true,
    "virtual": false,
    "origin": null,
    "icon": "🌐",
    "capabilities": {
      "website": false
    }
  },
  {
    "id": "6f253bc0-1c60-4715-9aff-60e99fc4c44f",
    "name": "pokemon",
    "created": "2016-08-28T22:18:01.534Z",
    "non_member_read": true,
    "non_member_write": false,
    "member_write": true,
    "description": "Where Pikachu is hiding",
    "website": null,
    "member_count": 1,
    "listable": true,
    "virtual": false,
    "origin": {
      "lat": 43.61909,
      "lon": -84.31337,
      "zoom": 18
    },
    "capabilities": {
      "website": false
    }
  }
]

The search query parameter is string of characters used for searching. Only maps with a description field matching all of the terms will be returned. Note that common English words, such as "the", are ignored when searching.

List Subscribed Maps

Request

GET /v2/users/{user_id}/subscribed
Accept: application/json
Authorization: Basic HASH

Successful Response

[
  {
    "id": "22d36af1-b3b9-4d82-977d-6b821c678f87",
    "name": "global",
    "created": "2016-08-25T04:45:01.713Z",
    "non_member_read": true,
    "non_member_write": true,
    "member_write": true,
    "description": "Where the fun happens",
    "website": "https://example.org",
    "listable": true,
    "virtual": false,
    "member_level": 2,
    "origin": null,
    "icon": "🌐",
    "capabilities": {
      "website": false
    }
  },
  {
    "id": "6f253bc0-1c60-4715-9aff-60e99fc4c44f",
    "name": "pokemon",
    "created": "2016-08-28T22:18:01.534Z",
    "non_member_read": true,
    "non_member_write": false,
    "member_write": true,
    "description": "Where Pikachu is hiding",
    "website": null,
    "member_count": 1,
    "listable": true,
    "virtual": false,
    "member_level": 3,
    "origin": {
      "lat": 43.61909,
      "lon": -84.31337,
      "zoom": 18
    },
    "capabilities": {
      "website": false
    }
  }
]

This endpoint gets a list of all maps that the user has subscribed to. This includes maps the user has created, and other maps that the user has subscribed to. This endpoint will only work if the user_id in the URL matches the ID of the user making the request. In other words, one user can't get a list of maps that another user has subscribed to.

The member_level attribute is the membership level of the given user. See the membership documentation for more information.

Get Map

Request

GET /v2/maps/{map_name}
Accept: application/json

Successful Response

{
  "id": "99999999-a868-4016-bba6-3e6dcf74e1fe",
  "name": "glbra-stem-pipeline",
  "created": "2021-10-15T23:06:00.102Z",
  "non_member_read": true,
  "non_member_write": false,
  "member_write": false,
  "virtual": false,
  "listable": true,
  "description": "Home of the Great Lakes Bay Region Alliance STEM Passport program! Users joining in Michigan are automatically subscribed. Also known as the MI GLBRA STEM Pipeline.",
  "capabilities": {
    "image": true,
    "beacon": true,
    "social": true,
    "address": true,
    "checkin": "https://www.stempipeline.com/home/privacy-policy",
    "website": true,
    "markdown": true,
    "nocluster": true,
    "permanent": true
  },
  "origin": {
    "lat": 43.852,
    "lon": -84.197,
    "zoom": 8
  },
  "member_count": 101,
  "is_member": true,
  "is_admin": true,
  "has_password": false,
  "website": "https://www.stempipeline.com/",
  "post_sort": "alphabetical",
  "icon": "⚛️"
}

This request performs a lookup for information about a single map.

Create Map

Request

POST /v2/maps
Authorization: Basic HASH
Content-Type: application/json
Accept: application/json
{
  "name": "pokemongo",
  "listable": true,
  "password": null,
  "non_member_read": true,
  "non_member_write": false,
  "member_write": true,
  "description": "foo",
  "website": "https://example.org",
  "icon": "👽",
  "origin": {
    "lat": 43.61909,
    "lon": -84.31337,
    "zoom": 18
  },
  "capabilities": {
    "permanent": false,
    "beacon": false,
    "image": false,
    "address": false,
    "social": false,
    "website": false,
    "checkin": "https://example.org/terms-of-service.html",
    "markdown": false
  }
}

Successful Response

{
  "id": "6f253bc0-1c60-4715-9aff-60e99fc4c44f",
  "name": "pokemongo",
  "listable": true,
  "created": "2016-08-28T22:32:22.416Z",
  "non_member_read": true,
  "non_member_write": false,
  "member_write": true,
  "description": "foo",
  "website": "https://example.org",
  "member_count": 1,
  "is_member": true,
  "is_admin": true,
  "icon": "👽",
  "origin": {
    "lat": 43.61909,
    "lon": -84.31337,
    "zoom": 18
  },
  "capabilities": {
    "website": true,
    "checkin": "https://example.org/terms-of-service.html"
  }
}

The origin field is optional, and if omitted when creating a map, will default to a value of null. If present, it must contain a lat and lon pair. The zoom value is optional, and should be an integer between 9 and 18, inclusive. If omitted, the zoom value is omitted in the response. The origin is used to signal to the client where a default location and zoom level should be used when rendering a map.

The icon field is optional. It represents both the icon of the map itself, as well as a default icon for posts that are added to that map.

Delete Map

Request

DELETE /v2/maps/{map_name}
Authorization: Basic HASH
Content-Type: application/json
Accept: application/json

Successful Response

{
  "id": "6f253bc0-1c60-4715-9aff-60e99fc4c44f",
  "name": "pokemongo",
  "listable": true,
  "created": "2016-08-28T22:32:22.416Z",
  "non_member_read": true,
  "non_member_write": false,
  "member_write": true,
  "description": "foo",
  "website": "https://example.org",
  "member_count": 0,
  "origin": {
    "lat": 43.61909,
    "lon": -84.31337,
    "zoom": 18
  },
  "capabilities": {
    "website": false
  }
}

Must be an administrator of a map when deleting.

Calling this endpoint will delete all content associated with the map. This includes posts, post replies, and post check-ins.

Update Map

Request

PATCH /v2/maps/{map_name}
Authorization: Basic HASH
Content-Type: application/json
Accept: application/json
{
  "listable": true,
  "password": null,
  "non_member_read": true,
  "non_member_write": false,
  "member_write": true,
  "description": "foo",
  "website": "https://example.org",
  "icon": "👽",
  "origin": {
    "lat": 43.61909,
    "lon": -84.31337,
    "zoom": 18
  },
  "capabilities": {
    "permanent": false,
    "beacon": false,
    "image": false,
    "address": false,
    "social": false,
    "website": false,
    "checkin": "https://example.org/terms-of-service.html",
    "markdown": false
  }
}

Successful Response

{
  "id": "6f253bc0-1c60-4715-9aff-60e99fc4c44f",
  "name": "pokemongo",
  "listable": true,
  "created": "2016-08-28T22:32:22.416Z",
  "non_member_read": true,
  "non_member_write": false,
  "member_write": true,
  "description": "foo",
  "website": "https://example.org",
  "member_count": 1,
  "is_member": true,
  "is_admin": true,
  "icon": "👽",
  "origin": {
    "lat": 43.61909,
    "lon": -84.31337,
    "zoom": 18
  },
  "capabilities": {
    "website": true,
    "checkin": "https://example.org/terms-of-service.html"
  }
}

Must be an administrator of a map when updating.

This endpoint allows for modifications to an existing map. Only fields that are changing need to be provided in the request; existing fields will be merged into the final resulting map object. For fields that are to be "removed" or cleared out, provided a value of null. For example, setting password to null will remove the password for the map.

See Create Map for a list of acceptable fields and their restrictions.

If provided, id and name and created fields must exactly equal their existing values. If the values differ then the update operation will fail. In other words, it's not possible to rename a map or change its creation time.

If additional non-free capabilities are desired then you'll need to first contact support.

The response object is a representation of the channel.

Post Endpoints

A post is the basic unit of content that is created on Map Buddy. Posts on Map Buddy are pretty similar to the colloquial post concept used by other popular social media platforms. A large differentiator though is that Map Buddy posts don't appear in an algorithmically controlled timeline. Instead, they are primarily discovered based on physical location of a user.

Here is a post with many features enabled.

Post Fields

Field Type Purpose
id String Unique identifier
ch String The name of the map this post belongs to
level Number/String The loudness / Post Level
lat Number The latitude of the post
lon Number The longitude of the post
uid UUID The post author's user_id
uname UUID The post author's username
created Date The timestamp for when the post was created
expire Date/null The timestamp for when the post will expire from the map
message String The text representation of the post
nsfw Boolean/null Whether or not the post contains adult content
icon String/null A single emoji representation of the post
metadata Object/null An object containing additional post metadata
replies Number/null If numeric, the number of replies on the post

As a bit of historical context, maps on Map Buddy were previously known as "Channels". This is why the map name field is called ch. A future version of the API might rename this field to map.

Posts on Map Buddy cannot be paginated upon. Instead, they need to be searched using a geolocation.

It's possible to create posts that are permanent. When this happens, they will have an expired value of null. Such posts will not expire from the map, though they can still be removed if the author deletes the post. Permanent posts are useful to represent data that isn't based on a time; for example, a post representing the Statue of Liberty landmark would make sense to be permanent.

The level field can be a number from 1 though 5, or the string 'beacon'. It can never be omitted.

The nsfw field can be missing for old posts. If the field is missing on a post then assume a value of false. The icon field can also be missing for posts of any age. When missing, the Map Buddy app displays a black circle emoji. Feel free to do what feel appropriate for your application.

Post Metadata

The optional metadata fields are only supported by certain paid maps. If provided, only certain fields are permitted. Here's a list of the fields, the acceptable type, and their correlating map capability:

Metadata Capability Type Description
address address String Street address, used for driving directions
website website String URL to a website
calendar website String URL to a calendar
instagram social String URL to an Instagram profile
facebook social String URL to an Facebook page
twitter social String URL to an Twitter page
image image String URL to an image to embed in the post (UNUSED)
markdown markdown String Markdown text to be rendered below post
checkin checkin Integer Enables checking in and specifies max distance in meters
email contact String Email address to be emailed
phone contact String Phone number to be called

Most posts will eventually expire. When a post expires, it is merely removed from the map, but otherwise remains in the system. If a user has added a post to their favorites, it will still be listed after expiration. A URL to open the post will also still open. Replies can still be made and the post can be manually deleted.

There are two types of posts both when it comes to visibility and when it comes to making API requests to find them. Regular Posts have a numeric level and their visibility depends on the users location. Essentially, a regular post can only be seen by a user who is within a certain distance. This is useful for scavenger hunts and hiding things in the world. The other type of posts are Beacon Posts and have a level of 'beacon' and can be seen regardless of where the user is located. The Map Buddy app requests beacon posts as the user pans around the map. The shapes of post data are the same but have two different endpoints for querying.

This is a screenshot of a post with metadata:

Screenshot of Post with Metadata

In this screenshot, the large text at the top is the message field, which all posts have. The bcn text is the post level, meaning this is a beacon post. The @tlhunter text is the author, the #glba-stem-pipeline is the name of the channel. So far, that data is universal across all posts.

The smaller font paragraph of text is the metadata.markdown field. The four blue buttons on the bottom of the message are each links to an external service. The globe is metadata.website, the calendar is metadata.calendar, the blue "F" is metadata.facebook, and the turning sign indicator is metadata.address, which has been parsed into a URL to either Google Maps or Apple Maps, depending on the platform. Finally, the orange marker button triggers a modal prompting the user to Check-in, and depends on metadata.checkin.

List Posts by Location

Request

GET /v2/maps/{map_name}/posts?lat={latitude}&lon={longitude}&timerange={timerange}&limit={limit}
Accept: application/json

Response

{
  "lat": 43.39286023385813,
  "lon": -83.98384382366594,
  "type": "post",
  "maps": [ "global" ],
  "posts": [
    {
      "id": "4nVmTolRhPeElt53RFdde7",
      "lat": 37.772336,
      "lon": -122.390819,
      "ch": "global",
      "uid": "dc1caf1b-f7d5-47c2-bfb9-88329f3bbe19",
      "uname": "tlhunter",
      "message": "Lorem Ipsum",
      "level": 1,
      "created": "2016-09-07T06:48:08.075Z",
      "expires": "2016-09-14T06:48:08.075Z",
      "nsfw": false,
      "metadata": {
        "address": "1313 Mockingbird Ln San Francisco CA 94107",
        "website": "https://mapbuddy.app/",
        "instagram": "https://www.instagram.com/sfmoma/",
        "facebook": "https://www.facebook.com/tlhunter",
        "twitter": "https://twitter.com/tlhunter",
        "checkin": 200,
        "markdown": "## Markdown\n\nDocument"
      }
    }
  ]
}

The lat and lon fields are just returned from the request. The type field is always set to "post". The maps field contains a list of maps used for the query. This is only interested in the case of a home request where multiple subscribed maps may be returned. The posts array is the list of matched posts.

The limit field is optional and defaults to 100. It limits the number of posts returned in the query. The value must be at least 1.

The timerange field is optional and defaults to "forever". It is used to filter the return values based on the created timestamp. Here are the possible values:

Time Range Description
forever Return all posts regardless of creation date, default
hour Return posts younger than 1 hour
day Return posts younger than 24 hours
week Return posts younger than 7 days
month Return posts younger than 31 days
year Return posts younger than 366 days

List Beacons by Location

Request

GET /v2/maps/{map_name}/posts?lat={latitude}&lon={longitude}&type=beacon&radius={radius}&timerange={timerange}
GET /v2/maps/{map_name}/posts?lat={latitude}&lon={longitude}&type=beacon&width={width}height={height}&timerange={timerange}
Accept: application/json

Response

{
  "lat": 43.39286023385813,
  "lon": -83.98384382366594,
  "type": "beacon",
  "maps": [ "glbra-stem-pipeline" ],
  "posts": [
    {
      "id": "2soimSb6U6SG3hO12kTZis",
      "level": "beacon",
      "lat": 43.41128367002303,
      "lon": -83.9525522325348,
      "ch": "stem",
      "uid": "9515bf8d-39d0-423d-b18d-df24de27e188",
      "uname": "tlhunter",
      "created": "2021-11-05T03:04:14.714Z",
      "expire": null,
      "nsfw": false,
      "icon": "🐒",
      "message": "Saginaw Children's Zoo",
      "metadata": { }
    }
  ]
}

Beacons are exactly like a normal post except they have a "level": "beacon" field. Beacons don't get updated as commonly as normal posts. They are always visible and queries don't depend on the user's position but instead the map viewport position.

The lat and lon parameters refer to the center of the map viewport. The type parameter is always set to beacon. When searching for beacon posts the request can either search by using a radius with a center point, or a box with a center point.

Radius Search: The radius parameter is the radius from the center for which to query for beacons, measured in meters. The app previously used the distance from the center to a corner of the map as a radius. This meant it received unnecessary posts outside of the viewport. The max radius is 100km (radius=100000), and will error if the radius is larger. Calculating viewport height and width isn't always easy so the API still supports this method.

Box Search: The width and height parameters represent a box around which to query for beacons, measured in meters. The box is centered upon the lat/lon pair. So having width=1000 and height=800 will retrieve beacons within 500 meters East and West and 400 meters North and South of the center. The maximum range is 200km (width=200000&height=200000), and the request will error if the box is larger.

Create Post

Request

POST /v2/maps/{map_name}/posts
Authorization: Basic HASH
Content-Type: application/json
Accept: application/json
{
  "lat": 37,
  "lon": -122,
  "message": "hi",
  "level": 1,
  "permanent": false,
  "early_expire": 7200,
  "nsfw": true,
  "place_id": "ChIJWeOzhJQVkFQRnSmyf-NV_gA",
  "created": "2016-09-07T06:48:08.075Z",
  "icon": "🙀",
  "replies": true,
  "favorite": true,
  "metadata": {
    "address": "1313 Mockingbird Ln San Francisco CA 94107",
    "website": "https://mapbuddy.app/",
    "instagram": "https://www.instagram.com/sfmoma/",
    "facebook": "https://www.facebook.com/tlhunter",
    "twitter": "https://twitter.com/tlhunter"
  }
}

Response

{
  "id": "4nVmTolRhPeElt53RFdde7",
  "lat": 37,
  "lon": -122,
  "ch": "global",
  "uid": "dc1caf1b-f7d5-47c2-bfb9-88329f3bbe19",
  "uname": "tlhunter",
  "message": "hi",
  "level": 1,
  "created": "2016-09-07T06:48:08.075Z",
  "expires": "2016-09-14T06:48:08.075Z",
  "nsfw": true,
  "icon": "🙀",
  "replies": 0,
  "metadata": {
    "address": "1313 Mockingbird Ln San Francisco CA 94107",
    "website": "https://mapbuddy.app/",
    "instagram": "https://www.instagram.com/sfmoma/",
    "facebook": "https://www.facebook.com/tlhunter",
    "twitter": "https://twitter.com/tlhunter"
  }
}

The optional permanent field can only be set to true for if the map has the permanent capability enabled. If provided, the post will never expire, and the return object will have an expired field set to null.

The optional early_expire field is a positive integer representing the number of seconds until the message will expire from the map. It's useful for causing a message to expire earlier than is expected for the given level. For example, when creating a level 2 post, the default expiration is six months, but you might want to create a post at that level that expires in one month. The request will fail if the value is greater than the expiration time for that level. A value of 0 is ignored. The field cannot be set if the permanent field has been set to true.

The optional created field lets you specify a backdated creation time. The date should follow the ISO-8601 format. This field can only be set to a value in the past. The date must be of the year 2020 or greater. This field should be omitted if a user is currently creating a post.

The optional nsfw field defaults to false. It allows a user to flag the post as being only appropriate for adults. Filtering of posts with this flag is done entirely client-side.

The optional place_id field refers to the Google Maps Place ID of the location, if such a place exists and if it makes sense to correlate the two. For example, if the user is creating a post that represents Golden Goat Coffee, then it makes sense to provide the place ID for Golden Goat Coffee, if known.

The optional icon field accepts a single visual emoji. The API will silently mutate the emoji that is passed in if required. For example, passing in two emojis will result in the first emoji being selected. Passing in brand new emojis, which are often composites of multiple emojis, may result in the first of the emojis being selected. Passing in no emojis results in an empty field. If the icon field is omitted or becomes empty and the map has an associated icon then that value is used instead.

The optional replies field is a boolean and defaults to false. By setting it to true, replies are enabled for the post. Note that the response object uses a number for the replies field.

The optional favorite field is a boolean and defaults to false. By setting it to true, the post will automatically be added to the user's list of favorite posts. The Map Buddy mobile app sets this value to true. This value is not returned in the response.

Get Post

Request

GET /v2/maps/{map_name}/posts/{post_id}
Accept: application/json

Response

{
  "id": "4nVmTolRhPeElt53RFdde7",
  "lat": 37,
  "lon": -122,
  "message": "hi",
  "level": 1,
  "ch": "global",
  "uid": "dc1caf1b-f7d5-47c2-bfb9-88329f3bbe19",
  "uname": "tlhunter",
  "created": "2016-09-07T06:48:08.075Z",
  "expires": "2016-09-14T06:48:08.075Z",
  "nsfw": false,
  "icon": "🙀",
  "replies": 10,
  "metadata": {
    "address": "1313 Mockingbird Ln San Francisco CA 94107",
    "website": "https://mapbuddy.app/",
    "instagram": "https://www.instagram.com/sfmoma/",
    "facebook": "https://www.facebook.com/tlhunter",
    "twitter": "https://twitter.com/tlhunter"
  }
}

This request retrieves a single post.

Delete Post

Request

DELETE /v2/maps/{map_name}/posts/{post_id}
Accept: application/json
Authorization: Basic HASH

Response

{
  "id": "4nVmTolRhPeElt53RFdde7",
  "lat": 37,
  "lon": -122,
  "message": "hi",
  "level": 1,
  "ch": "global",
  "uid": "dc1caf1b-f7d5-47c2-bfb9-88329f3bbe19",
  "uname": "tlhunter",
  "created": "2016-09-07T06:48:08.075Z",
  "expires": "2016-09-14T06:48:08.075Z",
  "nsfw": false,
  "icon": "🙀",
  "metadata": {
    "address": "1313 Mockingbird Ln San Francisco CA 94107",
    "website": "https://mapbuddy.app/",
    "instagram": "https://www.instagram.com/sfmoma/",
    "facebook": "https://www.facebook.com/tlhunter",
    "twitter": "https://twitter.com/tlhunter"
  }
}

This request deletes a single post.

Note that the request must be made by either an administrator of the map or by the author of the post. Once deleted, a post is both removed from the map and removed from any other user's favorites. It's impossible to open the post in any manner.

Update Post

Request

PATCH /v2/maps/{map_name}/posts/{post_id}
Content-Type: application/json
Accept: application/json
Authorization: Basic HASH
{
  "message": "foo bar",
  "lat": 40,
  "lon": -120,
  "level": 2
}

Response

{
  "id": "4nVmTolRhPeElt53RFdde7",
  "lat": 40,
  "lon": -120,
  "message": "foo bar",
  "level": 2,
  "ch": "global",
  "uid": "dc1caf1b-f7d5-47c2-bfb9-88329f3bbe19",
  "uname": "tlhunter",
  "created": "2016-09-07T06:48:08.075Z",
  "expires": "2016-09-14T06:48:08.075Z",
  "nsfw": false,
  "icon": "🙀"
}

Only admins can modify posts. This includes both Super Admins, who can modify any post, as well as Map Admins, who can only modify their own posts within a map that they are an administrator of. Regular users cannon modify posts.

Only fields that are being modified need to be supplied in the request object. The existing post will be retrieved, the object being provided in the request will be merged into the existing post (overriding existing fields), and the resulting object is saved.

If an id field is provided in the request then it must match the {post_id} in the request path. Basically this means you cannot change the post ID. If the IDs don't match then the request will fail.

If a ch field is provided in the request then it must match the existing ch field in the post. Basically this means posts cannot be moved from one map to another. If it doesn't match then the request will fail.

If expire or created fields are provided in the request, they must perfectly equal the existing values. Basically this means that expiration and creation times cannot change. If they don't match then the request will fail.

With this in mind, it's probably best to entirely omit id, ch, expire, and created fields from the request body to reduce chances that the request fails. For example, you don't want a slight date serialization change to fail your request.

Report Post

Request

POST /v2/maps/{map_name}/posts/{post_id}/reports
Content-Type: application/json
Accept: application/json
{
  "lat": 43.454956,
  "lon": -84.728111,
  "reason": "This is spam"
}

Successful Response

{
  "post_id": "3bvPRXJRKGZJsmnjYdln1e",
  "created": "2022-01-29T02:20:03.184Z",
  "reporter_id": "9515bf8d-39d0-423d-b18d-df24de27e188",
  "lat": 43.454956,
  "lon": -84.728111,
  "reason": "This is spam"
}

Error Response (Already Reported)

{
  "error": {
    "code": "report_collision",
    "message": "You have already reported this post"
  }
}

This endpoint allows a user to report a post. An administrator is notified of such reports so that they may delete the post, modify the post to be set to NSFW, or even ban the user.

The user may supply a reason as to why they're flagging the post using the reason field.

Note that the user supplies their geolocation to prevent abuse of the system. This isn't checked by code but may be looked at by an administrator.

Favorite Endpoints

By adding a post to a user's list of favorites, the user essentially bookmarks the post to make it easy to return to later. Recall that posts are usually only discovered by being physically at a location. This means most users can't interact with a post once they leave the area. By favoriting a post, a user is able to continue the conversation when they go home.

A user can favorite posts that they create themselves, and posts that others create.

List Favorite Posts

Request

GET /v2/users/{user_id}/favorites
Accept: application/json
Authorization: Basic HASH

Successful Response

[
  {
    "id": "6rsYpXlXYo3itjmuzOBDV8",
    "level": 5,
    "lat": 37.780456,
    "lon": -122.3942944,
    "ch": "global",
    "uid": "7444155f-83c9-4416-ab5c-1de6e04c9242",
    "created": "2022-06-29T00:38:59.783Z",
    "expire": "2022-06-29T01:38:59.783Z",
    "message": "Here is another message",
    "username": "tlhunter",
    "metadata": null,
    "nsfw": false,
    "replies": 0,
    "place_id": null,
    "icon": "🔥"
  },
  {
    "id": "5pOLjs6lxgBsvxOl35lK2z",
    "level": 1,
    "lat": 37.7875028,
    "lon": -122.4003223,
    "ch": "global",
    "uid": "7444155f-83c9-4416-ab5c-1de6e04c9242",
    "created": "2022-06-27T19:28:26.393Z",
    "expire": "2024-06-26T19:28:26.393Z",
    "message": "Here is a message",
    "username": "tlhunter",
    "metadata": null,
    "nsfw": true,
    "replies": 0,
    "place_id": null,
    "icon": "🌐"
  }
]

This request returns a list of posts that a user has favorited. A user can only make requests for their favorite posts, not for any other user.

Favorite Post

Request

POST /v2/maps/{map_name}/posts/{post_id}/favorite
Content-Type: application/json
Accept: application/json
Authorization: Basic HASH
{
  "user_id": "9515bf8d-39d0-423d-b18d-df24de27e188"
}

Successful Response

{
  "post_id": "3DhRpcFMvWiaZtK4lVWyzU",
  "map_id": "8a927dd7-7c3d-4ffa-8a65-69aa4e3f7e41",
  "user_id": "9515bf8d-39d0-423d-b18d-df24de27e188"
}

Error Response, Already Favorited

{
  "error": {
    "code": "favorite_collision",
    "message": "You have already favorited this post"
  }
}

This request adds a post to the user's list of favorite posts. The request body contains a single field, user_id, which is the User ID (not the username) of the current user. The response returns some information which is for the most part not that useful. If the response was successful then the favoriting was successful.

Unfavorite Post

Request

DELETE /v2/maps/{map_name}/posts/{post_id}/favorite/{user_id}
Accept: application/json
Authorization: Basic HASH

Successful Response

{
  "post_id": "3DhRpcFMvWiaZtK4lVWyzU",
  "map_id": "8a927dd7-7c3d-4ffa-8a65-69aa4e3f7e41",
  "user_id": "9515bf8d-39d0-423d-b18d-df24de27e188"
}

Error Response, Was Not Favorited

{
  "error": {
    "code": "not_favorited",
    "message": "User has not favorited this post"
  }
}

This request removes a post from the user's list of favorites. The response isn't that useful, but if it succeeds then the post was successfully removed, and if it errors, the post wasn't removed.

Check Favorite Post

Request

GET /v2/maps/{map_name}/posts/{post_id}/favorite/{user_id}
Accept: application/json
Authorization: Basic HASH

Successful Response, Is Favorited

{
  "post_id": "3DhRpcFMvWiaZtK4lVWyzU",
  "map_id": "8a927dd7-7c3d-4ffa-8a65-69aa4e3f7e41",
  "user_id": "9515bf8d-39d0-423d-b18d-df24de27e188"
}

404 Error Response, Is Not Favorited

{
  "error": {
    "code": "not_favorited",
    "message": "User has not favorited this post"
  }
}

This request lets you know if the given user has favorited a post. Like the previous endpoints in this section, the response body isn't too important. If the request succeeds, the user has favorited the post. If the request fails, the user has not favorited the post.

Post Reply Endpoints

Posts can optionally have replies enabled on them. Enabling replies allows other users to respond to and hold conversations related to a given post. For example, a post about a party might include users discussing what items to bring or neighbors complaining about noise levels.

Replies are enabled by setting replies: true when creating a post. To confirm if a post has replies enabled or not it will have a replies field with a value that is a number. The number represents the number of replies, with 0 meaning no replies. This means you can't just perform a truthy-ness check on the replies field.

List Post Replies

Request

GET /v2/maps/{map_name}/posts/{post_id}/replies
Accept: application/json

Successful Response

[
  {
    "id": "fcbce952-1d33-4a19-a442-44b76f73a78c",
    "user_id": "7444155f-83c9-4416-ab5c-1de6e04c9242",
    "username": "tlhunter",
    "created": "2022-06-29T19:59:11.345Z",
    "message": "omg"
  },
  {
    "id": "6c54504d-c86d-4207-a93e-c5198b5142cd",
    "user_id": "7444155f-83c9-4416-ab5c-1de6e04c9242",
    "username": "tlhunter",
    "created": "2022-06-29T19:59:20.205Z",
    "message": "whoa"
  }
]

This request returns a list of replies for a given post. Replies are ordered based on ascending created timestamp, meaning older posts come first and later posts come last. The id field is a UUID for the post reply itself. The user_id and username fields belong to the author of that post. The created field is the time the reply was made. The message field is the text for that reply.

Create Post Reply

Request

POST /v2/maps/{map_name}/posts/{post_id}/replies
Accept: application/json
Authorization: Basic HASH
Content-Type: application/json
{
  "message": "Ground Control to Major Tom"
}

Successful Response

{
  "id": "b9af55e0-cb96-44f0-831b-836a9fc1aa93",
  "post_id": "6g7QSWmaMn1COMCl1Efwes",
  "map_id": "abdcc5ec-d15a-4a01-af23-4cdd1348a578",
  "user_id": "7444155f-83c9-4416-ab5c-1de6e04c9242",
  "created": "2022-06-29T20:04:06.783Z",
  "message": "Ground Control to Major Tom"
}

This endpoint allows the user to add a new response to a post. The request only needs a message field, which is the message to be added to the post. The response object isn't too important, but the success of the response will let you know if the reply was created or not.

Delete Post Reply

Request

DELETE /v2/maps/{map_name}/posts/{post_id}/replies/{reply_id}
Accept: application/json
Authorization: Basic HASH

Successful Response

{
  "id": "b9af55e0-cb96-44f0-831b-836a9fc1aa93",
  "post_id": "6g7QSWmaMn1COMCl1Efwes",
  "map_id": "abdcc5ec-d15a-4a01-af23-4cdd1348a578",
  "user_id": "7444155f-83c9-4416-ab5c-1de6e04c9242",
  "created": "2022-06-29T20:04:06.783Z",
  "message": "Ground Control to Major Tom"
}

This endpoint allows a user to delete a reply to a post. The response isn't too important, but the success of the response will let you know if the reply was deleted or not.

Membership Endpoints

A membership is the association between a user and a map. A given membership can have different levels. Here is a list of the membership levels:

Level Name Description
1 Subscribed Posts appear on home screen but user doesn't have "member" permissions.
2 Member User gets posts on home screen and is considered a member for permissions.
3 Administrator User is also able to modify the map and perform other tasks.

When a user creates a map they will automatically gain a membership level of administrator. Numerically higher membership levels include the same capabilities as lower levels. Conceptually, a higher membership level number means higher permissions.

Posts that are made to a map that a user is subscribed to will appear on that user's #home map.

Note that user accounts have an admin flag. This flag translates into "Super Admin" status. Being an admin of a channel and a Super Admin are unrelated.

Subscribe to Map

Request

POST /v2/maps/{map_name}/members
Authorization: Basic HASH
Content-Type: application/json
Accept: application/json
{
  "user_id": "dc1caf1b-f7d5-47c2-bfb9-88329f3bbe19",
  "level": 1
}

201 Response

{
  "user_id": "dc1caf1b-f7d5-47c2-bfb9-88329f3bbe19",
  "map_id": "22d36af1-b3b9-4d82-977d-6b821c678f87",
  "level": 1
}

This endpoint can be used to subscribe the current user to a map. Note that the user_id field in the payload is redundant with the User ID provided with the Authorization header.

Get Membership

Request

GET /v2/maps/{map_name}/members/{user_id}
Authorization: Basic HASH
Accept: application/json

200 Response

{
  "user_id": "dc1caf1b-f7d5-47c2-bfb9-88329f3bbe19",
  "map_id": "22d36af1-b3b9-4d82-977d-6b821c678f87",
  "level": 1,
  "username": "tlhunter"
}

This request looks up membership information about the given user_id. This request only works on behalf of the currently authenticated user.

Unsubscribe from Map

Request

DELETE /v2/maps/{map_name}/members/{user_id}
Authorization: Basic HASH
Accept: application/json

200 Response

{
  "user_id": "dc1caf1b-f7d5-47c2-bfb9-88329f3bbe19",
  "map_id": "22d36af1-b3b9-4d82-977d-6b821c678f87",
  "level": 2
}

This request removes a user's membership from a given channel. This request only works for the currently authenticated user.

Note that there are currently no checks to see if any users remain as an administrator for the channel. This can result in a zombie channel that has no owner.

Check-in Endpoints

Check-ins are an advanced feature of Map Buddy. They are only available on maps with the checkin capability. Presently, this capability is a non-free feature. Contact us if this is a feature you would like to use.

The ability to check-in is configured on a per-post basis. When a user visits a real world location with a post that they can check-in to, they are then given the option to check-in by tapping a button in the Map Buddy interface. If the user is too far away, the check-in will fail.

Each post that can be checked-in to has a numeric range attached to it, measured in meters. When making a request to check-in, the client sends the user's current geolocation. The server then determines if the check-in fails or not.

Users must be authenticated to check-in to posts. Only map admins and Super Admins are able to retrieve check-in data. Users must always be prompted to agree to the terms of service for the owner of the map before being allowed to check-in. This is because privacy information is shared, like the user's location and when they checked-in. A URL for the terms of service is available in the map's capabilities.checkin field.

Check-in to Post

Request

POST /v2/maps/{map_name}/posts/{post_id}/checkins
Content-Type: application/json
Authorization: Basic HASH
{
  "lat": 43.454956,
  "lon": -84.728111
}

Successful Response

{
  "user_id": "7444155f-83c9-4416-ab5c-1de6e04c9242",
  "map_id": "99999999-a868-4016-bba6-3e6dcf74e1fe",
  "post_id": "4WstAzsAz8MI0E5rhJv2OA",
  "created": "2021-12-27T21:59:24.389Z",
  "lat": 43.454956,
  "lon": -84.728111
}

Error Response, Too Far Away

{
  "error": {
    "code": "checkin_too_far",
    "message": "You are too far from the location to check in! You are 3272956m away, while the max is 300m."
  }
}

This endpoint allows a user to check in to a post. The request requires the user's current geolocation. If the user is determined to be too far from the post then then check-in will fail.

Get All Post Check-ins

Request

GET /v2/maps/{map_name}/posts/{post_id}/checkins
Authorization: Basic HASH
Accept: application/json

Response

{
  "id": "4nVmTolRhPeElt53RFdde7",
  "lat": 37,
  "lon": -122,
  "ch": "global",
  "uid": "dc1caf1b-f7d5-47c2-bfb9-88329f3bbe19",
  "uname": "tlhunter",
  "message": "hi",
  "level": 1,
  "created": "2016-09-07T06:48:08.075Z",
  "expires": "2016-09-14T06:48:08.075Z",
  "metadata": {
    "address": "1313 Mockingbird Ln San Francisco CA 94107",
    "website": "https://mapbuddy.app/",
    "instagram": "https://www.instagram.com/sfmoma/",
    "facebook": "https://www.facebook.com/tlhunter",
    "twitter": "https://twitter.com/tlhunter"
  }
}

This endpoint is probably not all that useful. It gets a list of all check-ins for a specific post.

Get All Post Check-ins for Map

Request

GET /v2/maps/{map_name}/checkins?timezone=America/Los_Angeles
Accept: application/json
Accept: text/csv
Authorization: Basic HASH

Response (JSON)

[
  {
    "email": "tlhunter@gmail.com",
    "username": "tlhunter",
    "time_of_checkin": "2021-11-06T21:05:02.942Z",
    "message": "Mt. Pleasant Discovery Museum",
    "post_id": "1BZLMqfLEfHYNpqHAagAI4",
    "latitude": 43.598,
    "longitude": -84.745
  },
  {
    "email": "tlhunter@gmail.com",
    "username": "tlhunter",
    "time_of_checkin": "2021-11-06T21:02:57.387Z",
    "message": "CMU Center for Excellence in STEM Education ",
    "post_id": "45Xt2S0sak2QY5vtqK8jon",
    "latitude": 43.5848,
    "longitude": -84.77286
  }
]

Response (CSV)

email,username,time_of_checkin,message,post_id,latitude,longitude
tlhunter@gmail.com,tlhunter,"6/17/2022, 10:07:56 AM",Post Message Text,2tOmk7lkToJoFkrKAZkjsR,37.783,-122.396
tlhunter@gmail.com,tlhunter,"5/25/2022, 3:22:47 PM",Post Message Text,47gImG3wGiw4skPCBAgZjb,37.785,-122.398

This endpoint is used to generate periodical reports for customers. The response format can be chosen by providing an Accept header. The default behavior is to return JSON, but by specifying text/csv, a CSV document is returned. This is to make it easier to provide reports to customers.

Note that only super admins can currently make this request. Eventually customers / map admins will be able to make this request.

Note that fields containing a comma or a double quote will be automatically wrapped in double quotes. This is handled by a library and should always result in a document that can be opened by spreadsheet software.

When making a request for a CSV file, a query parameter timezone may be included. If so, it is used as the timezone. For example, ?timezone=America/Los_Angeles. When provided, the formatted dates will me set to this timezone. If omitted, it defaults to the PST timezone. This parameter doesn't affect the JSON response as it responds with ISO-8601 dates which convey a timezone.

Get User Check-ins

Request

GET /v2/users/{user_id}/checkins
Authorization: Basic HASH
Accept: application/json

Response

[
  {
    "user_id": "9515bf8d-39d0-423d-b18d-df24de27e188",
    "map_id": "99999999-a868-4016-bba6-3e6dcf74e1fe",
    "post_id": "1BZLMqfLEfHYNpqHAagAI4",
    "created": "2021-11-06T21:05:02.942Z",
    "lat": 43.598,
    "lon": -84.745,
    "map_name": "glbra-stem-pipeline"
  }
]

This endpoint returns a list of all check-ins that the user has made. It can only work on behalf of the currently authenticated user. The app currently makes this request when it first launches and then caches the result. The result is then used to display to the user if a particular post has been checked into or not.

The response is an array of check-in objects.

Batch Endpoints

These endpoints are very basic wrappers around existing endpoints. They allow a client to, for common scenarios, make a single request for data instead of multiple requests. These endpoints exist merely to deal with the shortcomings of a basic HTTP API. If this API were built using, say GraphQL, then these endpoints would not exist at all.

Get Map and List Posts

Request

GET /v2/batch/map-and-posts/{map_name}?lat={latitude}&lon={longitude}&timerange=forever
Accept: application/json

Response

{
  "map": {
    "id": "06993365-e85c-4076-b4c2-12f3256268e5",
    "name": "sfpd",
    "created": "2022-01-25T01:49:18.424Z",
    "non_member_read": true,
    "non_member_write": false,
    "member_write": false,
    "virtual": false,
    "listable": true,
    "description": "San Francisco Police Department (SFPD)",
    "capabilities": {
      "image": true,
      "beacon": true,
      "social": false,
      "address": true,
      "website": true,
      "markdown": true,
      "permanent": true
    },
    "origin": null,
    "member_count": 28,
    "is_member": true,
    "is_admin": true,
    "has_password": false,
    "website": "https://www.sanfranciscopolice.org/",
    "post_sort": null,
    "icon": "🚨"
  },
  "posts": {
    "lat": "37.786",
    "lon": "-122.404",
    "type": "beacon",
    "virtual": false,
    "maps": [
      "sfpd"
    ],
    "posts": [
      {
        "id": "1QtYNAHqUIKTn4k910TG9O",
        "level": "beacon",
        "lat": 37.780,
        "lon": -122.413,
        "ch": "sfpd",
        "uid": "bf108f95-452f-4efc-b699-94f6f47f3894",
        "uname": "bot",
        "created": "2022-06-28T03:47:00.000Z",
        "expire": "2022-07-28T03:47:00.000Z",
        "message": "License Plate, Found",
        "metadata": {
          "markdown": "Other Offenses | Case 220419851"
        },
        "nsfw": false,
        "replies": 0,
        "icon": "🚨"
      }
    ]
  }
}

This is a combination of the Get Map endpoint, assigned to map, and the List Posts by Location endpoint, assigned to posts. It should only be called when it's appropriate to get data from both endpoints at the same time, such as when first loading a map. Don't use this endpoint when continually polling for updated posts. For that, just use the List Posts by Location endpoint, as it will be faster.

The Map Buddy mobile app uses this endpoint when first opening a map. After that, the map data is cached, and lookups to List Posts by Location are made instead.

Get Post and List Replies

Request

GET /v2/batch/post-and-replies/{map_name}/{post_id}
Accept: application/json

Response

{
  "post": {
    "id": "259Uw9rRCTQtf6AFQcH7Ar",
    "level": "beacon",
    "lat": 37.789,
    "lon": -122.397,
    "ch": "sfpd",
    "uid": "bf108f95-452f-4efc-b699-94f6f47f3894",
    "created": "2022-06-20T00:00:00.000Z",
    "expire": "2022-07-20T00:00:00.000Z",
    "message": "Theft, From Person, >$950",
    "metadata": {
      "markdown": "Larceny Theft | Case 220402317"
    },
    "nsfw": false,
    "replies": 1,
    "place_id": null,
    "icon": null,
    "uname": "bot"
  },
  "favorited": true,
  "replies": [
    {
      "id": "8f3c7646-a350-49ac-a6ea-0abdd7d4c7a5",
      "user_id": "7444155f-83c9-4416-ab5c-1de6e04c9242",
      "username": "tlhunter",
      "created": "2022-07-01T03:07:49.611Z",
      "message": "Yikes!"
    }
  ]
}

This endpoint is a combination of the Get Post endpoint, assigned to post, the List Replies endpoint, assigned to replies, and finally a check to see if the post has been favorited or not, essentially a simplified version of the Check Favorite Post endpoint.

The Map Buddy mobile app uses this endpoint when loading the view post screen. This way, instead of making three requests, it only makes a single request.