The Anchor AppView provides a RESTful API for accessing location-based social check-ins from the AT Protocol network.
https://dropanchor.app/api
Note: All API endpoints are now under the /api/ namespace to separate them
from frontend routes.
Read Endpoints: No authentication required (public feeds)
Write Endpoints: Require OAuth authentication
All authenticated endpoints support cookie-based authentication (recommended):
Cookie: sid=Fe26.2**...sealed-session-token...**
Alternatively, Bearer token authentication is supported for compatibility:
Authorization: Bearer Fe26.2**...sealed-session-token...**
Recommendation: Use cookie-based authentication. After completing OAuth, create an HTTPCookie and add it to HTTPCookieStorage (iOS) or document.cookie (web). URLSession/fetch will automatically include cookies in requests.
To obtain authentication credentials:
/login (returns session cookies
automatically)See Creating Check-ins for authenticated endpoint examples.
All responses are JSON with proper CORS headers enabled.
{ "checkins": [...], "cursor": "2025-07-04T10:00:00Z" }
Each check-in in the response includes full profile information and optional fields:
{ "id": "3lbmo5gsjgv2f", "uri": "at://did:plc:wxex3wx5k4ctciupsv5m5stb/app.dropanchor.checkin/3lbmo5gsjgv2f", "author": { "did": "did:plc:wxex3wx5k4ctciupsv5m5stb", "handle": "tijs.social", "displayName": "Tijs", "avatar": "https://cdn.bsky.app/img/avatar/..." }, "text": "Great coffee here!", "createdAt": "2025-07-04T10:00:00Z", "coordinates": { "latitude": 37.7749, "longitude": -122.4194 }, "address": { "name": "Blue Bottle Coffee", "street": "1 Ferry Building", "locality": "San Francisco", "region": "CA", "country": "US", "postalCode": "94111" }, "category": "cafe", "categoryGroup": "Food & Drink", "categoryIcon": "☕", "image": { "thumbUrl": "https://pds.example.com/xrpc/com.atproto.sync.getBlob?did=...&cid=bafyrei...", "fullsizeUrl": "https://pds.example.com/xrpc/com.atproto.sync.getBlob?did=...&cid=bafyrei...", "alt": "Latte art at Blue Bottle" }, "fsq": { "fsqPlaceId": "4b5bc7b0f964a520e68928e3", "name": "Blue Bottle Coffee", "latitude": "37.7749", "longitude": "-122.4194" } }
Note: The image, fsq, category, categoryGroup, and categoryIcon
fields are optional.
{ "error": "Description of the error" }
Get check-ins within a specified radius of coordinates using spatial queries.
Endpoint: GET /api/nearby
Required Parameters:
lat: Latitude (decimal degrees)lng: Longitude (decimal degrees)Optional Parameters:
radius: Search radius in kilometers (default: 5, max: 50)limit: Number of results (default: 50, max: 100)Example Request:
curl "https://dropanchor.app/api/nearby?lat=52.3676&lng=4.9041&radius=10&limit=20"
Example Response:
{ "checkins": [ { "id": "record123", "uri": "at://did:plc:example123/app.dropanchor.checkin/record123", "author": { "did": "did:plc:example123", "handle": "alice.bsky.social" }, "text": "Great spot for lunch!", "createdAt": "2025-07-04T10:15:00Z", "coordinates": { "latitude": 52.3700, "longitude": 4.9000 }, "address": { "name": "Restaurant Brown", "street": "Herengracht 456", "locality": "Amsterdam", "region": "North Holland", "country": "Netherlands", "postalCode": "1017 CA" }, "distance": 2.34 } ], "center": { "latitude": 52.3676, "longitude": 4.9041 }, "radius": 10 }
Notes:
distance field is included in nearby results (in kilometers)Get all check-ins from a specific user.
Endpoint: GET /api/user
Required Parameters:
did: User's decentralized identifierOptional Parameters:
limit: Number of results (default: 50, max: 100)cursor: ISO timestamp for paginationExample Request:
curl "https://dropanchor.app/api/user?did=did:plc:example123&limit=10"
Example Response:
{ "checkins": [ { "id": "record123", "uri": "at://did:plc:example123/app.dropanchor.checkin/record123", "author": { "did": "did:plc:example123", "handle": "alice.bsky.social" }, "text": "Love this place!", "createdAt": "2025-07-04T10:15:00Z", "coordinates": { "latitude": 52.3676, "longitude": 4.9041 }, "address": { "name": "Cafe Central", "street": "Damrak 123", "locality": "Amsterdam", "region": "North Holland", "country": "Netherlands", "postalCode": "1012 LP" } } ], "user": { "did": "did:plc:example123" } }
Get check-ins from users that the specified user follows on Bluesky.
Endpoint: GET /api/following
Required Parameters:
user: User's DID to get following feed forOptional Parameters:
limit: Number of results (default: 50, max: 100)cursor: ISO timestamp for paginationExample Request:
curl "https://dropanchor.app/api/following?user=did:plc:example123&limit=10"
Example Response:
{ "checkins": [ { "id": "record456", "uri": "at://did:plc:friend789/app.dropanchor.checkin/record456", "author": { "did": "did:plc:friend789", "handle": "bob.bsky.social" }, "text": "Best pizza in town!", "createdAt": "2025-07-04T10:30:00Z", "coordinates": { "latitude": 52.3600, "longitude": 4.8800 }, "address": { "name": "Pizza Palace", "street": "Prinsengracht 789", "locality": "Amsterdam", "region": "North Holland", "country": "Netherlands", "postalCode": "1016 HT" } } ], "user": { "did": "did:plc:example123" }, "cursor": "2025-07-04T10:30:00Z" }
Note: Social graph data is synced daily from Bluesky. If no follows are found, returns an empty array.
Search for nearby places using OpenStreetMap data via the Overpass API. This endpoint enables location-based place discovery for check-ins.
Endpoint: GET /api/places/nearby
Required Parameters:
lat: Latitude coordinate (-90 to 90)lng: Longitude coordinate (-180 to 180)Optional Parameters:
radius: Search radius in meters (default: 300, max: 2000)categories: Comma-separated list of OSM categories (e.g.,
"amenity=cafe,amenity=restaurant")Example Request:
curl "https://dropanchor.app/api/places/nearby?lat=37.7749&lng=-122.4194&radius=500&categories=amenity%3Dcafe,amenity%3Drestaurant"
Example Response:
{ "places": [ { "id": "node:2895323815", "elementType": "node", "elementId": 2895323815, "name": "Blue Bottle Coffee", "latitude": 37.7749, "longitude": -122.4194, "address": { "name": "Blue Bottle Coffee", "street": "315 Linden St", "locality": "San Francisco", "region": "CA", "country": "US", "postalCode": "94102" }, "category": "cafe", "categoryGroup": "food_and_drink", "icon": "☕", "distanceMeters": 45.8, "formattedDistance": "46m", "tags": { "amenity": "cafe", "name": "Blue Bottle Coffee", "addr:street": "Linden St", "addr:housenumber": "315", "addr:city": "San Francisco", "addr:state": "CA", "addr:country": "US", "addr:postcode": "94102" } } ], "totalCount": 23, "searchRadius": 500, "categories": ["amenity=cafe", "amenity=restaurant"], "searchCoordinate": { "latitude": 37.7749, "longitude": -122.4194 } }
Place Categories:
Common category filters include:
amenity=restaurant, amenity=cafe, amenity=bar,
shop=supermarketamenity=cinema, tourism=attraction, leisure=parkleisure=fitness_centre, leisure=climbing,
leisure=swimming_poolshop=clothes, shop=electronics, shop=bookstourism=hotel, tourism=hostelamenity=bus_station, amenity=fuelNotes:
Get AppView health metrics.
Endpoint: GET /api/stats
Parameters: None
Example Request:
curl "https://dropanchor.app/api/stats"
Example Response:
{ "activeUsers": 89, "timestamp": "2025-07-04T10:45:00Z" }
Fields:
activeUsers: Number of users with active OAuth sessionstimestamp: Current server timestampNote: Statistics reflect active OAuth sessions, not total check-ins (which are stored in users' PDS servers).
Create a new check-in with optional image attachment.
Endpoint: POST /api/checkins
Authentication Required: Yes (OAuth via cookie or Bearer token)
Content-Type:
application/json (without image)multipart/form-data (with image)Request Body (JSON):
{ "place": { "latitude": 37.7749, "longitude": -122.4194, "name": "Blue Bottle Coffee", "address": { "name": "Blue Bottle Coffee", "street": "1 Ferry Building", "locality": "San Francisco", "region": "CA", "country": "US", "postalCode": "94111" } }, "message": "Great coffee here!" }
Request Body (FormData with image):
const formData = new FormData();
formData.append(
"place",
JSON.stringify({
latitude: 37.7749,
longitude: -122.4194,
name: "Blue Bottle Coffee",
address: {/* ... */},
}),
);
formData.append("message", "Great coffee here!");
formData.append("image", imageFile); // File object (JPEG, PNG, WebP, GIF)
formData.append("imageAlt", "Latte art at Blue Bottle");
Image Requirements:
Example Request (cURL with cookie - Recommended):
curl -X POST "https://dropanchor.app/api/checkins" \ -b "sid=YOUR_SESSION_TOKEN" \ -F "place={\"latitude\":37.7749,\"longitude\":-122.4194,\"name\":\"Blue Bottle Coffee\"}" \ -F "message=Great coffee here!" \ -F "image=@photo.jpg" \ -F "imageAlt=Latte art at Blue Bottle"
Alternative with Bearer token:
curl -X POST "https://dropanchor.app/api/checkins" \ -H "Authorization: Bearer YOUR_SESSION_TOKEN" \ -F "place={\"latitude\":37.7749,\"longitude\":-122.4194,\"name\":\"Blue Bottle Coffee\"}" \ -F "message=Great coffee here!" \ -F "image=@photo.jpg" \ -F "imageAlt=Latte art at Blue Bottle"
Note:
YOUR_SESSION_TOKENis obtained from the OAuth flow. See Authentication Guide for Anchor setup details, or the @tijs/atproto-oauth Mobile OAuth Guide for complete implementation details.
Example Request (Swift/iOS with cookie - Recommended):
// Assumes session cookie is already set in HTTPCookieStorage from OAuth flow // See Mobile OAuth API documentation for complete authentication setup var request = URLRequest(url: URL(string: "https://dropanchor.app/api/checkins")!) request.httpMethod = "POST" let boundary = UUID().uuidString request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") var body = Data() // Add place JSON body.append("--\(boundary)\r\n".data(using: .utf8)!) body.append("Content-Disposition: form-data; name=\"place\"\r\n\r\n".data(using: .utf8)!) body.append(placeJSON.data(using: .utf8)!) // Add message body.append("\r\n--\(boundary)\r\n".data(using: .utf8)!) body.append("Content-Disposition: form-data; name=\"message\"\r\n\r\n".data(using: .utf8)!) body.append("Great coffee here!".data(using: .utf8)!) // Add image body.append("\r\n--\(boundary)\r\n".data(using: .utf8)!) body.append("Content-Disposition: form-data; name=\"image\"; filename=\"photo.jpg\"\r\n".data(using: .utf8)!) body.append("Content-Type: image/jpeg\r\n\r\n".data(using: .utf8)!) body.append(imageData) // Add image alt text body.append("\r\n--\(boundary)\r\n".data(using: .utf8)!) body.append("Content-Disposition: form-data; name=\"imageAlt\"\r\n\r\n".data(using: .utf8)!) body.append("Latte art at Blue Bottle".data(using: .utf8)!) body.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!) request.httpBody = body // Cookie is automatically included by URLSession from HTTPCookieStorage let (data, _) = try await URLSession.shared.data(for: request)
Success Response (201 Created):
{ "success": true, "checkin": { "id": "3lbmo5gsjgv2f", "uri": "at://did:plc:wxex3wx5k4ctciupsv5m5stb/app.dropanchor.checkin/3lbmo5gsjgv2f", "author": { "did": "did:plc:wxex3wx5k4ctciupsv5m5stb", "handle": "tijs.social" }, "text": "Great coffee here!", "createdAt": "2025-07-04T10:00:00Z", "image": { "thumbUrl": "https://pds.example.com/xrpc/com.atproto.sync.getBlob?did=...&cid=...", "fullsizeUrl": "https://pds.example.com/xrpc/com.atproto.sync.getBlob?did=...&cid=...", "alt": "Latte art at Blue Bottle" } } }
Error Response (400 Bad Request):
{ "error": "Image too large (max 10MB)" }
Possible Errors:
401 Unauthorized: Missing or invalid authentication400 Bad Request: Invalid image format, size exceeded, or missing required
fields500 Internal Server Error: Failed to upload to PDS or create recordDelete a check-in (only the author can delete their own check-ins).
Endpoint: DELETE /api/checkins/:did/:rkey
Authentication Required: Yes (must be the check-in author)
Example Request (with cookie - Recommended):
curl -X DELETE "https://dropanchor.app/api/checkins/did:plc:wxex3wx5k4ctciupsv5m5stb/3lbmo5gsjgv2f" \ -b "sid=YOUR_SESSION_TOKEN"
Alternative with Bearer token:
curl -X DELETE "https://dropanchor.app/api/checkins/did:plc:wxex3wx5k4ctciupsv5m5stb/3lbmo5gsjgv2f" \ -H "Authorization: Bearer YOUR_SESSION_TOKEN"
Success Response (200 OK):
{ "success": true }
Error Responses:
401 Unauthorized: Not authenticated or not the author404 Not Found: Check-in doesn't exist500 Internal Server Error: Failed to delete from PDSinterface Checkin {
id: string; // Unique record identifier (rkey)
uri: string; // Full AT Protocol URI
author: {
did: string; // User's decentralized identifier
handle: string; // User's Bluesky handle
displayName?: string; // User's display name
avatar?: string; // User's avatar URL
};
text: string; // Check-in message/review
createdAt: string; // ISO timestamp
coordinates: {
// Location coordinates (from geo object)
latitude: number;
longitude: number;
};
address?: {
// Venue/location address
name?: string; // Venue name
street?: string; // Street address
locality?: string; // City
region?: string; // State/Province
country: string; // ISO 3166 country code (required)
postalCode?: string; // Postal/ZIP code
};
category?: string; // Place category (e.g., "cafe", "restaurant")
categoryGroup?: string; // Category group (e.g., "Food & Drink")
categoryIcon?: string; // Emoji icon for the category
image?: {
// Optional image attachment
thumbUrl: string; // Thumbnail URL (300KB max)
fullsizeUrl: string; // Full-size URL (2MB max)
alt?: string; // Alt text description
};
fsq?: {
// Optional Foursquare venue data
fsqPlaceId: string; // Foursquare place ID
name?: string; // Venue name from Foursquare
latitude?: string; // Latitude from Foursquare
longitude?: string; // Longitude from Foursquare
};
distance?: number; // Distance in km (only in nearby results)
}
The app.dropanchor.checkin record stored in the user's PDS:
interface CheckinRecord {
$type: "app.dropanchor.checkin";
text: string; // Check-in message (required, max 3000 chars)
createdAt: string; // ISO timestamp (required)
address: {
// Embedded address (required)
name?: string;
street?: string;
locality?: string;
region?: string;
postalCode?: string;
country: string; // ISO 3166 code (required)
};
geo: {
// Embedded coordinates (required)
latitude: string; // Decimal degrees as string
longitude: string; // Decimal degrees as string
altitude?: string;
name?: string;
};
category?: string; // Place category
categoryGroup?: string; // Category group
categoryIcon?: string; // Emoji icon
image?: {
// Optional image
thumb: Blob; // Thumbnail (max 300KB)
fullsize: Blob; // Full-size (max 2MB)
alt?: string; // Alt text
};
fsq?: {
// Optional Foursquare data
fsqPlaceId: string; // Required if fsq present
name?: string;
latitude?: string;
longitude?: string;
};
}
Note: Coordinates are stored as strings in the lexicon (for DAG-CBOR compliance) but returned as numbers in API responses for convenience.
| Status Code | Description |
|---|---|
| 200 | Success |
| 201 | Created - Check-in successfully created |
| 400 | Bad Request - Missing or invalid parameters |
| 401 | Unauthorized - Authentication required |
| 404 | Not Found - Resource doesn't exist |
| 429 | Too Many Requests - Rate limit exceeded |
| 500 | Internal Server Error |
{ "error": "lat and lng parameters required" }
{ "error": "Invalid latitude or longitude" }
{ "error": "Rate limit exceeded. Try again later." }
class AnchorAPI {
private baseURL = "https://dropanchor.app/api";
async getNearbyCheckins(lat: number, lng: number, radius = 5, limit = 50) {
const params = new URLSearchParams({
lat: lat.toString(),
lng: lng.toString(),
radius: radius.toString(),
limit: limit.toString(),
});
const response = await fetch(`${this.baseURL}/nearby?${params}`);
return response.json();
}
async getUserCheckins(did: string, limit = 50, cursor?: string) {
const params = new URLSearchParams({
did,
limit: limit.toString(),
...(cursor && { cursor }),
});
const response = await fetch(`${this.baseURL}/user?${params}`);
return response.json();
}
async getFollowingFeed(userDid: string, limit = 50, cursor?: string) {
const params = new URLSearchParams({
user: userDid,
limit: limit.toString(),
...(cursor && { cursor }),
});
const response = await fetch(`${this.baseURL}/following?${params}`);
return response.json();
}
async getNearbyPlaces(
lat: number,
lng: number,
radius = 300,
categories?: string[],
) {
const params = new URLSearchParams({
lat: lat.toString(),
lng: lng.toString(),
radius: radius.toString(),
...(categories && { categories: categories.join(",") }),
});
const response = await fetch(`${this.baseURL}/places/nearby?${params}`);
return response.json();
}
async getStats() {
const response = await fetch(`${this.baseURL}/stats`);
return response.json();
}
}
import Foundation class AnchorAPI { private let baseURL = "https://dropanchor.app/api" func getNearbyCheckins(lat: Double, lng: Double, radius: Double = 5, limit: Int = 50) async throws -> NearbyResponse { var components = URLComponents(string: "\(baseURL)/nearby")! components.queryItems = [ URLQueryItem(name: "lat", value: String(lat)), URLQueryItem(name: "lng", value: String(lng)), URLQueryItem(name: "radius", value: String(radius)), URLQueryItem(name: "limit", value: String(limit)) ] let (data, _) = try await URLSession.shared.data(from: components.url!) return try JSONDecoder().decode(NearbyResponse.self, from: data) } }
For API support and feature requests, please visit:
community.lexicon.location.address recordsfsq object for Foursquare venue data