Developers

Shutter Speed API and MCP

Read and edit your saved locations, lists and location photos from your own scripts, or connect an AI assistant like Claude or ChatGPT. Available on Pro.

Base URL
https://api.shutterspeed.art/api/v1
MCP
https://api.shutterspeed.art/api/v1/mcp

All requests and responses are JSON. Timestamps are ISO 8601 in UTC. Fields an endpoint doesn't take are refused with a 400, not ignored.

A successful call returns 201 for a create or add, 204 with no body for a delete, and 200 for everything else. Over MCP, the delete tools return {"ok":true}.

Authentication

API keys

Create a key in the app under Settings, Account, API & Connected Apps. The full key is shown once, right after you create it, so copy it somewhere safe. Keys start with ss_live_.

You can have up to 10 keys. Revoke one from the same screen and it stops working right away.

Send the key as a Bearer token on every request:

curl https://api.shutterspeed.art/api/v1/locations \
  -H "Authorization: Bearer ss_live_..."

OAuth for AI assistants

Assistants that support MCP's OAuth sign-in don't need a key. Add the MCP server URL as a custom connector, sign in to your Shutter Speed account when asked, and choose Allow.

If an assistant asks you for an OAuth client ID and secret, or can't finish signing in, use an API key instead wherever it lets you set a request header.

Each connected assistant is listed under Settings, Account, API & Connected Apps, where you can disconnect it.

Both kinds of access need an active Pro subscription. If Pro ends, requests return 403 PRO_REQUIRED until you renew.

Rate limits

Up to 120 requests every 60 seconds per account. Past that you get a 429 with code RATE_LIMITED and a Retry-After header with the seconds to wait.

Pagination

List endpoints take skip and limit (1 to 100, default 50) and return the page with the total count:

{
  "data": [
    "..."
  ],
  "skip": 0,
  "limit": 50,
  "total": 128
}

Locations

The spots you've saved in Location Planner, with their address, notes, lists and photos.

GET/locations

MCP: list_locations

List your saved locations, newest first. Filter by list or by a text match on name, address, city, state, country or notes.

  • Read only
Parameters for GET /locations
NameDescription
skipinteger, query, optionalRows to skip. Default 0.
limitinteger, query, optionalRows to return, 1 to 100. Default 50.
listIduuid, query, optionalOnly locations in this list.
qstring, query, optionalCase-insensitive text search across name, address, city, state, country and notes.

Request

curl https://api.shutterspeed.art/api/v1/locations \
  -H "Authorization: Bearer ss_live_..."

Response 200 OK

{
  "data": [
    {
      "id": "3f2a9c1e-7b4d-4e2a-9c61-0d8e5f4a7b21",
      "name": "Gapstow Bridge",
      "latitude": 40.7661,
      "longitude": -73.9742,
      "address": "Gapstow Bridge, New York, NY 10019",
      "streetNumber": null,
      "street": null,
      "unit": null,
      "city": "New York",
      "state": "NY",
      "country": "US",
      "zipCode": "10019",
      "notes": "Best at sunrise, from the west bank.",
      "googlePlaceId": "ChIJs1Ugm_ZYwokRu3kzF0cVxt4",
      "wikipediaPageId": null,
      "createdAt": "2026-09-12T14:03:27.000Z",
      "listIds": [
        "b81d47e0-2c3a-4f19-8e6d-5a9c0b7e3f42"
      ],
      "images": [
        {
          "id": "e4c7a2b9-1d6f-4a83-b5e0-9f2c8d1a6b73",
          "url": "https://d1dkd0xzfyqgiq.cloudfront.net/eyJidWNrZXQiOi...",
          "thumbnailUrl": "https://d1dkd0xzfyqgiq.cloudfront.net/eyJidWNrZXQiOi...",
          "width": 4032,
          "height": 3024,
          "uploaderOwnsRights": true,
          "position": 0
        }
      ]
    }
  ],
  "skip": 0,
  "limit": 50,
  "total": 1
}

GET/locations/:id

MCP: get_location

Get one of your saved locations, with its lists and photos.

  • Read only
Parameters for GET /locations/:id
NameDescription
iduuid, path, requiredThe location's id (path).

Request

curl https://api.shutterspeed.art/api/v1/locations/3f2a9c1e-7b4d-4e2a-9c61-0d8e5f4a7b21 \
  -H "Authorization: Bearer ss_live_..."

Response 200 OK

{
  "id": "3f2a9c1e-7b4d-4e2a-9c61-0d8e5f4a7b21",
  "name": "Gapstow Bridge",
  "latitude": 40.7661,
  "longitude": -73.9742,
  "address": "Gapstow Bridge, New York, NY 10019",
  "streetNumber": null,
  "street": null,
  "unit": null,
  "city": "New York",
  "state": "NY",
  "country": "US",
  "zipCode": "10019",
  "notes": "Best at sunrise, from the west bank.",
  "googlePlaceId": "ChIJs1Ugm_ZYwokRu3kzF0cVxt4",
  "wikipediaPageId": null,
  "createdAt": "2026-09-12T14:03:27.000Z",
  "listIds": [
    "b81d47e0-2c3a-4f19-8e6d-5a9c0b7e3f42"
  ],
  "images": [
    {
      "id": "e4c7a2b9-1d6f-4a83-b5e0-9f2c8d1a6b73",
      "url": "https://d1dkd0xzfyqgiq.cloudfront.net/eyJidWNrZXQiOi...",
      "thumbnailUrl": "https://d1dkd0xzfyqgiq.cloudfront.net/eyJidWNrZXQiOi...",
      "width": 4032,
      "height": 3024,
      "uploaderOwnsRights": true,
      "position": 0
    }
  ]
}

POST/locations

MCP: create_location

Save a new location from a name and coordinates.

Parameters for POST /locations
NameDescription
namestring, body, requiredDisplay name.
latitudenumber, body, required-90 to 90.
longitudenumber, body, required-180 to 180.
addressstring, body, optionalAddress part.
streetNumberstring, body, optionalAddress part.
streetstring, body, optionalAddress part.
unitstring, body, optionalAddress part.
citystring, body, optionalAddress part.
statestring, body, optionalAddress part.
countrystring, body, optionalAddress part.
zipCodestring, body, optionalAddress part.
notesstring, body, optionalPrivate notes, up to 5000 characters.
googlePlaceIdstring, body, optionalGoogle Place id, if known.
wikipediaPageIdinteger, body, optionalWikipedia page id, if known.
listIdsuuid[], body, optionalLists to add it to.

Request

curl -X POST https://api.shutterspeed.art/api/v1/locations \
  -H "Authorization: Bearer ss_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Gapstow Bridge",
    "latitude": 40.7661,
    "longitude": -73.9742
  }'

Response 201 Created

{
  "id": "3f2a9c1e-7b4d-4e2a-9c61-0d8e5f4a7b21",
  "name": "Gapstow Bridge",
  "latitude": 40.7661,
  "longitude": -73.9742,
  "address": "Gapstow Bridge, New York, NY 10019",
  "streetNumber": null,
  "street": null,
  "unit": null,
  "city": "New York",
  "state": "NY",
  "country": "US",
  "zipCode": "10019",
  "notes": "Best at sunrise, from the west bank.",
  "googlePlaceId": "ChIJs1Ugm_ZYwokRu3kzF0cVxt4",
  "wikipediaPageId": null,
  "createdAt": "2026-09-12T14:03:27.000Z",
  "listIds": [
    "b81d47e0-2c3a-4f19-8e6d-5a9c0b7e3f42"
  ],
  "images": [
    {
      "id": "e4c7a2b9-1d6f-4a83-b5e0-9f2c8d1a6b73",
      "url": "https://d1dkd0xzfyqgiq.cloudfront.net/eyJidWNrZXQiOi...",
      "thumbnailUrl": "https://d1dkd0xzfyqgiq.cloudfront.net/eyJidWNrZXQiOi...",
      "width": 4032,
      "height": 3024,
      "uploaderOwnsRights": true,
      "position": 0
    }
  ]
}

PATCH/locations/:id

MCP: update_location

Change a saved location's name, coordinates, address or notes.

  • Idempotent
Parameters for PATCH /locations/:id
NameDescription
iduuid, path, requiredThe location's id (path).
namestring, body, optionalDisplay name.
latitudenumber, body, optional-90 to 90.
longitudenumber, body, optional-180 to 180.
addressstring | null, body, optionalAddress part. null clears it.
streetNumberstring | null, body, optionalAddress part. null clears it.
streetstring | null, body, optionalAddress part. null clears it.
unitstring | null, body, optionalAddress part. null clears it.
citystring | null, body, optionalAddress part. null clears it.
statestring | null, body, optionalAddress part. null clears it.
countrystring | null, body, optionalAddress part. null clears it.
zipCodestring | null, body, optionalAddress part. null clears it.
notesstring | null, body, optionalPrivate notes. null clears them.

Request

curl -X PATCH https://api.shutterspeed.art/api/v1/locations/3f2a9c1e-7b4d-4e2a-9c61-0d8e5f4a7b21 \
  -H "Authorization: Bearer ss_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Gapstow Bridge"
  }'

Response 200 OK

{
  "id": "3f2a9c1e-7b4d-4e2a-9c61-0d8e5f4a7b21",
  "name": "Gapstow Bridge",
  "latitude": 40.7661,
  "longitude": -73.9742,
  "address": "Gapstow Bridge, New York, NY 10019",
  "streetNumber": null,
  "street": null,
  "unit": null,
  "city": "New York",
  "state": "NY",
  "country": "US",
  "zipCode": "10019",
  "notes": "Best at sunrise, from the west bank.",
  "googlePlaceId": "ChIJs1Ugm_ZYwokRu3kzF0cVxt4",
  "wikipediaPageId": null,
  "createdAt": "2026-09-12T14:03:27.000Z",
  "listIds": [
    "b81d47e0-2c3a-4f19-8e6d-5a9c0b7e3f42"
  ],
  "images": [
    {
      "id": "e4c7a2b9-1d6f-4a83-b5e0-9f2c8d1a6b73",
      "url": "https://d1dkd0xzfyqgiq.cloudfront.net/eyJidWNrZXQiOi...",
      "thumbnailUrl": "https://d1dkd0xzfyqgiq.cloudfront.net/eyJidWNrZXQiOi...",
      "width": 4032,
      "height": 3024,
      "uploaderOwnsRights": true,
      "position": 0
    }
  ]
}

DELETE/locations/:id

MCP: delete_location

Delete a saved location and its photos.

  • Destructive
  • Idempotent
Parameters for DELETE /locations/:id
NameDescription
iduuid, path, requiredThe location's id (path).

Request

curl -X DELETE https://api.shutterspeed.art/api/v1/locations/3f2a9c1e-7b4d-4e2a-9c61-0d8e5f4a7b21 \
  -H "Authorization: Bearer ss_live_..."

Response 204 No Content

No body.

PUT/locations/:id/lists

MCP: set_location_lists

Replace the set of lists a location belongs to.

  • Destructive
  • Idempotent
Parameters for PUT /locations/:id/lists
NameDescription
iduuid, path, requiredThe location's id (path).
listIdsuuid[], body, requiredEvery list it should be in. [] removes it from all.

Request

curl -X PUT https://api.shutterspeed.art/api/v1/locations/3f2a9c1e-7b4d-4e2a-9c61-0d8e5f4a7b21/lists \
  -H "Authorization: Bearer ss_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "listIds": [
      "b81d47e0-2c3a-4f19-8e6d-5a9c0b7e3f42"
    ]
  }'

Response 200 OK

{
  "id": "3f2a9c1e-7b4d-4e2a-9c61-0d8e5f4a7b21",
  "name": "Gapstow Bridge",
  "latitude": 40.7661,
  "longitude": -73.9742,
  "address": "Gapstow Bridge, New York, NY 10019",
  "streetNumber": null,
  "street": null,
  "unit": null,
  "city": "New York",
  "state": "NY",
  "country": "US",
  "zipCode": "10019",
  "notes": "Best at sunrise, from the west bank.",
  "googlePlaceId": "ChIJs1Ugm_ZYwokRu3kzF0cVxt4",
  "wikipediaPageId": null,
  "createdAt": "2026-09-12T14:03:27.000Z",
  "listIds": [
    "b81d47e0-2c3a-4f19-8e6d-5a9c0b7e3f42"
  ],
  "images": [
    {
      "id": "e4c7a2b9-1d6f-4a83-b5e0-9f2c8d1a6b73",
      "url": "https://d1dkd0xzfyqgiq.cloudfront.net/eyJidWNrZXQiOi...",
      "thumbnailUrl": "https://d1dkd0xzfyqgiq.cloudfront.net/eyJidWNrZXQiOi...",
      "width": 4032,
      "height": 3024,
      "uploaderOwnsRights": true,
      "position": 0
    }
  ]
}

Lists

The lists you group locations into. Deleting a list keeps its locations.

GET/lists

MCP: list_lists

List your location lists with how many locations each holds.

  • Read only
Parameters for GET /lists
NameDescription
skipinteger, query, optionalRows to skip. Default 0.
limitinteger, query, optionalRows to return, 1 to 100. Default 50.

Request

curl https://api.shutterspeed.art/api/v1/lists \
  -H "Authorization: Bearer ss_live_..."

Response 200 OK

{
  "data": [
    {
      "id": "b81d47e0-2c3a-4f19-8e6d-5a9c0b7e3f42",
      "name": "Central Park",
      "locationCount": 12
    }
  ],
  "skip": 0,
  "limit": 50,
  "total": 1
}

GET/lists/:id

MCP: get_list

Get one of your lists and every location in it (not paginated).

  • Read only
Parameters for GET /lists/:id
NameDescription
iduuid, path, requiredThe list's id (path).

Request

curl https://api.shutterspeed.art/api/v1/lists/b81d47e0-2c3a-4f19-8e6d-5a9c0b7e3f42 \
  -H "Authorization: Bearer ss_live_..."

Response 200 OK

{
  "id": "b81d47e0-2c3a-4f19-8e6d-5a9c0b7e3f42",
  "name": "Central Park",
  "locationCount": 1,
  "locations": [
    {
      "id": "3f2a9c1e-7b4d-4e2a-9c61-0d8e5f4a7b21",
      "name": "Gapstow Bridge",
      "latitude": 40.7661,
      "longitude": -73.9742,
      "address": "Gapstow Bridge, New York, NY 10019",
      "streetNumber": null,
      "street": null,
      "unit": null,
      "city": "New York",
      "state": "NY",
      "country": "US",
      "zipCode": "10019",
      "notes": "Best at sunrise, from the west bank.",
      "googlePlaceId": "ChIJs1Ugm_ZYwokRu3kzF0cVxt4",
      "wikipediaPageId": null,
      "createdAt": "2026-09-12T14:03:27.000Z",
      "listIds": [
        "b81d47e0-2c3a-4f19-8e6d-5a9c0b7e3f42"
      ],
      "images": [
        {
          "id": "e4c7a2b9-1d6f-4a83-b5e0-9f2c8d1a6b73",
          "url": "https://d1dkd0xzfyqgiq.cloudfront.net/eyJidWNrZXQiOi...",
          "thumbnailUrl": "https://d1dkd0xzfyqgiq.cloudfront.net/eyJidWNrZXQiOi...",
          "width": 4032,
          "height": 3024,
          "uploaderOwnsRights": true,
          "position": 0
        }
      ]
    }
  ]
}

POST/lists

MCP: create_list

Create a new location list.

Parameters for POST /lists
NameDescription
namestring, body, requiredList name.

Request

curl -X POST https://api.shutterspeed.art/api/v1/lists \
  -H "Authorization: Bearer ss_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Central Park"
  }'

Response 201 Created

{
  "id": "b81d47e0-2c3a-4f19-8e6d-5a9c0b7e3f42",
  "name": "Central Park",
  "locationCount": 0
}

PATCH/lists/:id

MCP: update_list

Rename a list.

  • Idempotent
Parameters for PATCH /lists/:id
NameDescription
iduuid, path, requiredThe list's id (path).
namestring, body, requiredNew name.

Request

curl -X PATCH https://api.shutterspeed.art/api/v1/lists/b81d47e0-2c3a-4f19-8e6d-5a9c0b7e3f42 \
  -H "Authorization: Bearer ss_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Central Park"
  }'

Response 200 OK

{
  "id": "b81d47e0-2c3a-4f19-8e6d-5a9c0b7e3f42",
  "name": "Central Park",
  "locationCount": 12
}

DELETE/lists/:id

MCP: delete_list

Delete a list. The locations in it are kept.

  • Destructive
  • Idempotent
Parameters for DELETE /lists/:id
NameDescription
iduuid, path, requiredThe list's id (path).

Request

curl -X DELETE https://api.shutterspeed.art/api/v1/lists/b81d47e0-2c3a-4f19-8e6d-5a9c0b7e3f42 \
  -H "Authorization: Bearer ss_live_..."

Response 204 No Content

No body.

Images

Photos on a location, in the order the app shows them.

GET/locations/:id/images

MCP: list_location_images

List a location's photos in display order, with full-size and thumbnail URLs.

  • Read only
Parameters for GET /locations/:id/images
NameDescription
iduuid, path, requiredThe location's id (path).

Request

curl https://api.shutterspeed.art/api/v1/locations/3f2a9c1e-7b4d-4e2a-9c61-0d8e5f4a7b21/images \
  -H "Authorization: Bearer ss_live_..."

Response 200 OK

[
  {
    "id": "a19f3c5d-8e2b-4c70-9d4a-6b1e7f0c2d58",
    "url": "https://d1dkd0xzfyqgiq.cloudfront.net/eyJidWNrZXQiOi...",
    "thumbnailUrl": "https://d1dkd0xzfyqgiq.cloudfront.net/eyJidWNrZXQiOi...",
    "width": 4032,
    "height": 3024,
    "uploaderOwnsRights": true,
    "position": 0
  },
  {
    "id": "e4c7a2b9-1d6f-4a83-b5e0-9f2c8d1a6b73",
    "url": "https://d1dkd0xzfyqgiq.cloudfront.net/eyJidWNrZXQiOi...",
    "thumbnailUrl": "https://d1dkd0xzfyqgiq.cloudfront.net/eyJidWNrZXQiOi...",
    "width": 4032,
    "height": 3024,
    "uploaderOwnsRights": true,
    "position": 1
  }
]

POST/locations/:id/images/from-url

MCP: add_location_image_from_url

Add a photo to a location by giving a public https image URL. The server downloads and stores it.

  • Fetches a URL
Parameters for POST /locations/:id/images/from-url
NameDescription
iduuid, path, requiredThe location's id (path).
urlstring, body, requiredPublic https URL of a JPEG, PNG, WebP or HEIC image, up to 10 MB.
uploaderOwnsRightsboolean, body, requiredtrue if you took the photo or have the rights to it.

Request

curl -X POST https://api.shutterspeed.art/api/v1/locations/3f2a9c1e-7b4d-4e2a-9c61-0d8e5f4a7b21/images/from-url \
  -H "Authorization: Bearer ss_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/photos/gapstow-bridge.jpg",
    "uploaderOwnsRights": true
  }'

Response 201 Created

{
  "id": "e4c7a2b9-1d6f-4a83-b5e0-9f2c8d1a6b73",
  "url": "https://d1dkd0xzfyqgiq.cloudfront.net/eyJidWNrZXQiOi...",
  "thumbnailUrl": "https://d1dkd0xzfyqgiq.cloudfront.net/eyJidWNrZXQiOi...",
  "width": 4032,
  "height": 3024,
  "uploaderOwnsRights": true,
  "position": 0
}

POST/locations/:id/images/upload-url

REST only

Start a direct upload: returns an image id and a presigned URL to PUT the file to, valid for 5 minutes.

Parameters for POST /locations/:id/images/upload-url
NameDescription
iduuid, path, requiredThe location's id (path).
contentTypestring, body, requiredimage/jpeg, image/png, image/webp, image/heic or image/heif.
contentLengthinteger, body, requiredFile size in bytes, up to 10 MB.
uploaderOwnsRightsboolean, body, requiredtrue if you took the photo or have the rights to it.

Request

curl -X POST https://api.shutterspeed.art/api/v1/locations/3f2a9c1e-7b4d-4e2a-9c61-0d8e5f4a7b21/images/upload-url \
  -H "Authorization: Bearer ss_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "contentType": "image/jpeg",
    "contentLength": 2483120,
    "uploaderOwnsRights": true
  }'

Response 201 Created

{
  "imageId": "e4c7a2b9-1d6f-4a83-b5e0-9f2c8d1a6b73",
  "uploadUrl": "https://shutterspeed-images.s3.amazonaws.com/...?X-Amz-Signature=...",
  "expiresInSeconds": 300
}

POST/locations/:id/images/:imageId/finalize

REST only

Finish a direct upload after the PUT succeeds. Converts HEIC and records the size.

  • Idempotent
Parameters for POST /locations/:id/images/:imageId/finalize
NameDescription
iduuid, path, requiredThe location's id (path).
imageIduuid, path, requiredThe image's id (path).

Request

curl -X POST https://api.shutterspeed.art/api/v1/locations/3f2a9c1e-7b4d-4e2a-9c61-0d8e5f4a7b21/images/e4c7a2b9-1d6f-4a83-b5e0-9f2c8d1a6b73/finalize \
  -H "Authorization: Bearer ss_live_..."

Response 200 OK

{
  "id": "e4c7a2b9-1d6f-4a83-b5e0-9f2c8d1a6b73",
  "url": "https://d1dkd0xzfyqgiq.cloudfront.net/eyJidWNrZXQiOi...",
  "thumbnailUrl": "https://d1dkd0xzfyqgiq.cloudfront.net/eyJidWNrZXQiOi...",
  "width": 4032,
  "height": 3024,
  "uploaderOwnsRights": true,
  "position": 0
}

PUT/locations/:id/images/order

MCP: reorder_location_images

Set the display order of a location's photos. Send every photo id, first to last.

  • Destructive
  • Idempotent
Parameters for PUT /locations/:id/images/order
NameDescription
iduuid, path, requiredThe location's id (path).
imageIdsuuid[], body, requiredAll of the location's photo ids in the new order.

Request

curl -X PUT https://api.shutterspeed.art/api/v1/locations/3f2a9c1e-7b4d-4e2a-9c61-0d8e5f4a7b21/images/order \
  -H "Authorization: Bearer ss_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "imageIds": [
      "a19f3c5d-8e2b-4c70-9d4a-6b1e7f0c2d58",
      "e4c7a2b9-1d6f-4a83-b5e0-9f2c8d1a6b73"
    ]
  }'

Response 200 OK

[
  {
    "id": "a19f3c5d-8e2b-4c70-9d4a-6b1e7f0c2d58",
    "url": "https://d1dkd0xzfyqgiq.cloudfront.net/eyJidWNrZXQiOi...",
    "thumbnailUrl": "https://d1dkd0xzfyqgiq.cloudfront.net/eyJidWNrZXQiOi...",
    "width": 4032,
    "height": 3024,
    "uploaderOwnsRights": true,
    "position": 0
  },
  {
    "id": "e4c7a2b9-1d6f-4a83-b5e0-9f2c8d1a6b73",
    "url": "https://d1dkd0xzfyqgiq.cloudfront.net/eyJidWNrZXQiOi...",
    "thumbnailUrl": "https://d1dkd0xzfyqgiq.cloudfront.net/eyJidWNrZXQiOi...",
    "width": 4032,
    "height": 3024,
    "uploaderOwnsRights": true,
    "position": 1
  }
]

DELETE/locations/:id/images/:imageId

MCP: delete_location_image

Delete one photo from a location.

  • Destructive
  • Idempotent
Parameters for DELETE /locations/:id/images/:imageId
NameDescription
iduuid, path, requiredThe location's id (path).
imageIduuid, path, requiredThe image's id (path).

Request

curl -X DELETE https://api.shutterspeed.art/api/v1/locations/3f2a9c1e-7b4d-4e2a-9c61-0d8e5f4a7b21/images/e4c7a2b9-1d6f-4a83-b5e0-9f2c8d1a6b73 \
  -H "Authorization: Bearer ss_live_..."

Response 204 No Content

No body.

Adding photos

There are two ways to add a photo. If the image is already online, POST its https URL to /images/from-url and the server downloads it. This one also works over MCP.

To upload a file from disk, use the direct upload, which takes three steps:

  1. POST /locations/:id/images/upload-url with the file's content type and size. You get back an image id and a presigned upload URL, valid for 5 minutes.
  2. PUT the file to that URL: exactly contentLength bytes, with the same Content-Type you sent in step 1. A different size or type is refused.
  3. POST /locations/:id/images/:imageId/finalize. The photo then shows up on the location. Finalizing before the PUT has finished returns 404.

Direct upload is REST only, since an assistant can't send file bytes to a presigned URL.

MCP server

The MCP server uses Streamable HTTP and accepts either OAuth or an API key. It exposes the same operations as the REST API, minus the direct upload.

https://api.shutterspeed.art/api/v1/mcp

Claude.ai and Claude Desktop

  1. Open Settings, then Connectors, and choose Add custom connector.
  2. Paste the MCP URL and save.
  3. Choose Connect, sign in to Shutter Speed and choose Allow.

ChatGPT

  1. Open Settings, then Apps & Connectors, and turn on developer mode under Advanced.
  2. Create a connector with the MCP URL and OAuth authentication.
  3. Sign in to Shutter Speed when asked and choose Allow.
  4. If ChatGPT asks for a client ID and secret instead, it can't register itself with Shutter Speed. Use an API key with a client that lets you set headers.

Claude Code

With OAuth (run /mcp in Claude Code afterwards to sign in):

claude mcp add --transport http shutterspeed https://api.shutterspeed.art/api/v1/mcp

Or with an API key:

claude mcp add --transport http shutterspeed https://api.shutterspeed.art/api/v1/mcp \
  --header "Authorization: Bearer ss_live_..."

Cursor and other clients

Clients that can send a header work with an API key. In Cursor, add the server to mcp.json with your key in the headers (this skips OAuth entirely):

{
  "mcpServers": {
    "shutterspeed": {
      "url": "https://api.shutterspeed.art/api/v1/mcp",
      "headers": {
        "Authorization": "Bearer ss_live_..."
      }
    }
  }
}

Tools

Each MCP tool maps to one REST endpoint and takes the same fields. Tools are marked read only, destructive or idempotent, so an assistant can tell a lookup from a delete before it asks you.

MCP tools and their REST endpoints
Tool
list_locationsGET/locationsList your saved locations, newest first. Filter by list or by a text match on name, address, city, state, country or notes.
get_locationGET/locations/:idGet one of your saved locations, with its lists and photos.
create_locationPOST/locationsSave a new location from a name and coordinates.
update_locationPATCH/locations/:idChange a saved location's name, coordinates, address or notes.
delete_locationDELETE/locations/:idDelete a saved location and its photos.
set_location_listsPUT/locations/:id/listsReplace the set of lists a location belongs to.
list_listsGET/listsList your location lists with how many locations each holds.
get_listGET/lists/:idGet one of your lists and every location in it (not paginated).
create_listPOST/listsCreate a new location list.
update_listPATCH/lists/:idRename a list.
delete_listDELETE/lists/:idDelete a list. The locations in it are kept.
list_location_imagesGET/locations/:id/imagesList a location's photos in display order, with full-size and thumbnail URLs.
add_location_image_from_urlPOST/locations/:id/images/from-urlAdd a photo to a location by giving a public https image URL. The server downloads and stores it.
reorder_location_imagesPUT/locations/:id/images/orderSet the display order of a location's photos. Send every photo id, first to last.
delete_location_imageDELETE/locations/:id/images/:imageIdDelete one photo from a location.

Errors

Errors come back with a non-2xx status and a JSON body like this:

{
  "error": "Location not found",
  "code": "NOT_FOUND"
}
Error codes
CodeMeaning
400VALIDATION_FAILEDThe body or query didn't validate, including any field the endpoint doesn't take (unknown fields are refused, not ignored), or the body isn't valid JSON. error says which field.
401UNAUTHORIZEDMissing, unknown or revoked API key or access token.
403PRO_REQUIREDThe account isn't on Pro.
404NOT_FOUNDNo such location, list or image in your account, or a direct upload finalized before its PUT arrived.
409CONFLICTThe change conflicts with current data, for example a stale photo order or the photo limit.
413PAYLOAD_TOO_LARGEThe JSON body is over 512 KB.
429RATE_LIMITEDMore than 120 requests in 60 seconds. The Retry-After header says how many seconds to wait.
500INTERNAL_ERRORSomething went wrong on our side. Retry later.
503UNAVAILABLEPhoto storage is temporarily unavailable. Retry later.

Privacy

The API and MCP server only ever see your own spots. Community spots and other people's saved locations aren't reachable through either.

Questions or a bug in the API? Contact us.