| title | Usage Manual | |
|---|---|---|
| description | Guide for using OSM Notes API. | |
| version | 1.0.0 | |
| last_updated | 2026-01-25 | |
| author | AngocA | |
| tags |
|
|
| audience |
|
|
| project | OSM-Notes-API | |
| status | active |
Guide for using OSM Notes API.
All requests MUST include a User-Agent header with specific format:
User-Agent: <AppName>/<Version> (<Contact>)
Components:
<AppName>: Application name (letters, numbers, hyphens, dots)<Version>: Application version<Contact>: REQUIRED - Email or project URL
Valid Examples:
User-Agent: MyOSMApp/1.0 (contact@example.com)
User-Agent: Terranote/1.0 (https://github.com/Terranote/terranote-core)
User-Agent: ResearchTool/0.5 (researcher@university.edu)
Invalid Examples:
User-Agent: MyApp/1.0 # ❌ Missing contact
User-Agent: MyApp # ❌ Missing version and contact
User-Agent: MyApp/1.0 (invalid) # ❌ Invalid contact
- Anonymous: 50 requests per 15 minutes per IP + User-Agent combination
- Authenticated: 1000 requests/hour (when OAuth is available in Phase 5)
- Detected bots: 10 requests/hour (when anti-abuse middleware is implemented)
Rate limiting is enforced per IP address and User-Agent combination, meaning:
- Different applications (different User-Agent) from the same IP have separate limits
- Same application from different IPs have separate limits
- Health check endpoint (
/health) is excluded from rate limiting
Response headers include rate limiting information (standard headers):
RateLimit-Limit: 50
RateLimit-Remaining: 49
RateLimit-Reset: 1234567890
When rate limit is exceeded, you'll receive a 429 Too Many Requests response:
{
"error": "Too Many Requests",
"message": "Rate limit exceeded. Maximum 50 requests per 15 minutes allowed.",
"statusCode": 429
}Best practices:
- Implement exponential backoff when receiving 429 responses
- Monitor the
RateLimit-Remainingheader to avoid hitting limits - Use authenticated requests (OAuth) when available for higher limits
The API automatically detects and blocks:
- Known AI agents: Require OAuth authentication (403 Forbidden without OAuth)
- Examples: ChatGPT, GPT-4, Claude, Google Bard, GitHub Copilot, Perplexity, etc.
- Solution: Authenticate using OpenStreetMap OAuth to access the API
- Known bots: Very restrictive rate limiting (10 requests/hour)
- Examples: curl, wget, python-requests, Go http client, Scrapy, etc.
- These tools are allowed but with very low rate limits to prevent abuse
AI Detection: If you're using an AI agent, you must authenticate with OSM OAuth. Without authentication, you'll receive:
{
"error": "Forbidden",
"message": "AI agents require OAuth authentication. Please authenticate using OpenStreetMap OAuth to access this API.",
"statusCode": 403
}Bot Detection: Known bots are automatically detected and subject to restrictive rate limiting (10 requests/hour). To avoid this:
- Use a proper User-Agent header with contact information
- Format:
<AppName>/<Version> (<Contact>) - Example:
MyBot/1.0 (bot@example.com)instead ofcurl/7.68.0
http://localhost:3000/notes-api/v1
GET /healthVerifies the status of the API and its dependencies.
Example:
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
http://localhost:3000/healthResponse:
{
"status": "healthy",
"timestamp": "2025-12-24T13:00:00.000Z",
"database": {
"status": "up",
"responseTime": 15
},
"redis": {
"status": "not_configured"
}
}Status Values:
healthy: All services are operationaldegraded: Some optional services are down (e.g., Redis)unhealthy: Critical services are down (e.g., database)
Get detailed information about a specific note.
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
http://localhost:3000/notes-api/v1/notes/12345Response:
{
"data": {
"note_id": 12345,
"latitude": 4.6097,
"longitude": -74.0817,
"status": "open",
"created_at": "2024-01-15T10:30:00Z",
"closed_at": null,
"id_user": 67890,
"id_country": 42,
"comments_count": 3
}
}Error Responses:
400 Bad Request: Invalid note ID format404 Not Found: Note does not exist500 Internal Server Error: Server error
Get all comments for a specific note.
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
http://localhost:3000/notes-api/v1/notes/12345/commentsResponse:
{
"data": [
{
"comment_id": 1,
"note_id": 12345,
"user_id": 67890,
"username": "test_user",
"action": "opened",
"created_at": "2024-01-15T10:30:00Z",
"text": "This is a test note"
},
{
"comment_id": 2,
"note_id": 12345,
"user_id": 67891,
"username": "another_user",
"action": "commented",
"created_at": "2024-01-15T11:00:00Z",
"text": "I can help with this"
}
],
"count": 2
}Error Responses:
400 Bad Request: Invalid note ID format500 Internal Server Error: Server error
Search notes with various filters and pagination.
Basic Search:
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
"http://localhost:3000/notes-api/v1/notes?status=open&limit=10"With Filters:
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
"http://localhost:3000/notes-api/v1/notes?country=42&status=open&date_from=2024-01-01&date_to=2024-12-31&page=1&limit=20"Query Parameters:
country(number): Filter by country IDstatus(string): Filter by status (open,closed,reopened)user_id(number): Filter by user IDdate_from(string): Filter notes created from this date (ISO format:YYYY-MM-DD)date_to(string): Filter notes created until this date (ISO format:YYYY-MM-DD)bbox(string): Filter by bounding box (format:min_lon,min_lat,max_lon,max_lat)page(number): Page number (default: 1, minimum: 1)limit(number): Results per page (default: 20, maximum: 100, minimum: 1)
Response:
{
"data": [
{
"note_id": 12345,
"latitude": 4.6097,
"longitude": -74.0817,
"status": "open",
"created_at": "2024-01-15T10:30:00Z",
"closed_at": null,
"id_user": 67890,
"id_country": 42,
"comments_count": 3
},
{
"note_id": 12346,
"latitude": 4.6100,
"longitude": -74.0820,
"status": "open",
"created_at": "2024-01-16T10:30:00Z",
"closed_at": null,
"id_user": 67891,
"id_country": 42,
"comments_count": 1
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 250,
"total_pages": 13
},
"filters": {
"country": 42,
"status": "open",
"date_from": "2024-01-01",
"date_to": "2024-12-31",
"page": 1,
"limit": 20
}
}Error Responses:
400 Bad Request: Invalid parameters (invalid status, invalid page/limit values)500 Internal Server Error: Server error
Examples:
Search open notes in Colombia:
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
"http://localhost:3000/notes-api/v1/notes?country=42&status=open"Search notes by user:
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
"http://localhost:3000/notes-api/v1/notes?user_id=67890"Search notes in a bounding box:
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
"http://localhost:3000/notes-api/v1/notes?bbox=-74.1,4.6,-74.0,4.7"The advanced search feature extends the basic search with text search capabilities and logical operators (AND/OR) for combining multiple filters.
Text Search: Search for notes containing specific text in their comments:
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
"http://localhost:3000/notes-api/v1/notes?text=test"Logical Operators: Combine multiple filters using AND (default) or OR operators:
AND Operator (default - all conditions must match):
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
"http://localhost:3000/notes-api/v1/notes?country=42&status=open&operator=AND"OR Operator (any condition can match):
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
"http://localhost:3000/notes-api/v1/notes?country=42&status=open&operator=OR"Combining Text Search with Filters:
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
"http://localhost:3000/notes-api/v1/notes?text=test&country=42&status=open&operator=AND"Advanced Query Parameters:
text(string): Search for text in note comments (1-500 characters, case-insensitive)operator(string): Logical operator to combine filters (ANDorOR, default:AND)- All standard search parameters are also supported (
country,status,user_id,date_from,date_to,bbox,page,limit)
When Advanced Search is Used:
Advanced search is automatically enabled when either text or operator parameters are provided. When advanced search is used:
- Text search searches within note comments
- Filters can be combined with AND or OR operators
- Standard search features (pagination, filters) remain available
Examples:
Search notes with text "help" in comments:
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
"http://localhost:3000/notes-api/v1/notes?text=help"Search notes in Colombia OR Spain with text "fix":
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
"http://localhost:3000/notes-api/v1/notes?country=42&country=43&text=fix&operator=OR"Note: When using OR operator with multiple values of the same filter (e.g., multiple countries), you may need to make separate requests or use the text search combined with other filters.
Get detailed user profile with analytics and statistics.
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
http://localhost:3000/notes-api/v1/users/12345Response:
{
"data": {
"dimension_user_id": 123,
"user_id": 12345,
"username": "example_user",
"history_whole_open": 100,
"history_whole_closed": 50,
"history_whole_commented": 75,
"avg_days_to_resolution": 5.5,
"resolution_rate": 50.0,
"user_response_time": 2.3,
"days_since_last_action": 5,
"applications_used": [
{
"application_id": 1,
"application_name": "JOSM",
"count": 80
}
],
"collaboration_patterns": {
"mentions_given": 10,
"mentions_received": 5,
"replies_count": 20,
"collaboration_score": 0.75
},
"countries_open_notes": [
{
"rank": 1,
"country": "Colombia",
"quantity": 50
}
],
"hashtags": ["#MapColombia", "#MissingMaps"],
"date_starting_creating_notes": "2020-01-15",
"date_starting_solving_notes": "2020-02-01",
"last_year_activity": null,
"working_hours_of_week_opening": [],
"activity_by_year": {}
}
}Error Responses:
400 Bad Request: Invalid user ID format404 Not Found: User does not exist500 Internal Server Error: Server error
Example:
# Get user profile
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
http://localhost:3000/notes-api/v1/users/12345Get detailed country profile with analytics and statistics.
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
http://localhost:3000/notes-api/v1/countries/42Response:
{
"data": {
"dimension_country_id": 45,
"country_id": 42,
"country_name": "Colombia",
"country_name_en": "Colombia",
"country_name_es": "Colombia",
"iso_alpha2": "CO",
"history_whole_open": 1000,
"history_whole_closed": 800,
"avg_days_to_resolution": 7.2,
"resolution_rate": 80.0,
"notes_health_score": 75.5,
"new_vs_resolved_ratio": 1.2,
"notes_backlog_size": 50,
"notes_created_last_30_days": 100,
"notes_resolved_last_30_days": 80,
"users_open_notes": [
{
"rank": 1,
"user_id": 12345,
"username": "top_user",
"quantity": 50
}
],
"applications_used": [],
"hashtags": [],
"activity_by_year": {},
"working_hours_of_week_opening": []
}
}Error Responses:
400 Bad Request: Invalid country ID format404 Not Found: Country does not exist500 Internal Server Error: Server error
Example:
# Get country profile
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
http://localhost:3000/notes-api/v1/countries/42Get global statistics and analytics for all OSM notes.
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
http://localhost:3000/notes-api/v1/analytics/globalResponse:
{
"data": {
"dimension_global_id": 1,
"history_whole_open": 1000000,
"history_whole_closed": 800000,
"currently_open_count": 200000,
"avg_days_to_resolution": 5.5,
"resolution_rate": 80.0,
"notes_created_last_30_days": 5000,
"notes_resolved_last_30_days": 4500,
"active_users_count": 10000,
"notes_backlog_size": 50000,
"applications_used": [
{
"application_id": 1,
"application_name": "JOSM",
"count": 500000
}
],
"top_countries": [
{
"rank": 1,
"country_id": 42,
"country_name": "Colombia",
"notes_count": 100000
}
]
}
}Error Responses:
404 Not Found: Global analytics not found500 Internal Server Error: Server error
Example:
# Get global analytics
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
http://localhost:3000/notes-api/v1/analytics/globalThe trends endpoint provides temporal analysis of notes activity for users, countries, or globally.
Endpoint: GET /notes-api/v1/analytics/trends
Query Parameters:
type(required): Type of entity to get trends for. Must be one of:users: Get trends for a specific usercountries: Get trends for a specific countryglobal: Get global trends
user_id(required iftype=users): User ID to get trends forcountry_id(required iftype=countries): Country ID to get trends for
Example Request - User Trends:
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
"http://localhost:3000/notes-api/v1/analytics/trends?type=users&user_id=12345"Example Response - User Trends:
{
"type": "users",
"entity_id": 12345,
"entity_name": "example_user",
"trends": [
{
"year": "2020",
"open": 10,
"closed": 5
},
{
"year": "2021",
"open": 20,
"closed": 15
},
{
"year": "2022",
"open": 30,
"closed": 25
}
],
"working_hours": [0, 1, 2, 3, 4, 5, 6]
}Example Request - Country Trends:
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
"http://localhost:3000/notes-api/v1/analytics/trends?type=countries&country_id=42"Example Response - Country Trends:
{
"type": "countries",
"entity_id": 42,
"entity_name": "Colombia",
"trends": [
{
"year": "2020",
"open": 1000,
"closed": 800
},
{
"year": "2021",
"open": 1200,
"closed": 1000
}
],
"working_hours": [0, 1, 2, 3, 4, 5, 6]
}Example Request - Global Trends:
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
"http://localhost:3000/notes-api/v1/analytics/trends?type=global"Example Response - Global Trends:
{
"type": "global",
"trends": [
{
"year": "2020",
"open": 100000,
"closed": 80000
},
{
"year": "2021",
"open": 120000,
"closed": 100000
}
]
}Response Fields:
type: Type of entity (users,countries, orglobal)entity_id: ID of the entity (only for users and countries)entity_name: Name of the entity (only for users and countries)trends: Array of trend entries, each containing:year: Year as a string (e.g., "2020")open: Number of notes opened in that yearclosed: Number of notes closed in that year
working_hours: Array of 168 numbers representing activity by hour of week (24 hours × 7 days) (only for users and countries)
Error Responses:
400 Bad Request: Missing or invalid parameters404 Not Found: Entity not found500 Internal Server Error: Server error
Note: Trends are sorted by year in ascending order. The working_hours field is optional and may not be present for all entities.
Compare multiple users or countries side-by-side by their metrics.
GET /notes-api/v1/analytics/comparison?type=<type>&ids=<ids>Query Parameters:
type(string, required): Type of entities to compare. Valid values:users: Compare userscountries: Compare countries
ids(string, required): Comma-separated list of IDs to compare (maximum 10 IDs)
Examples:
Compare two users:
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
"http://localhost:3000/notes-api/v1/analytics/comparison?type=users&ids=12345,67890"Compare multiple countries:
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
"http://localhost:3000/notes-api/v1/analytics/comparison?type=countries&ids=42,43,44"Compare single user:
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
"http://localhost:3000/notes-api/v1/analytics/comparison?type=users&ids=12345"Response (Users Comparison):
{
"type": "users",
"entities": [
{
"user_id": 12345,
"username": "user1",
"history_whole_open": 100,
"history_whole_closed": 50,
"history_whole_commented": 75,
"avg_days_to_resolution": 5.5,
"resolution_rate": 50.0,
"user_response_time": 2.3
},
{
"user_id": 67890,
"username": "user2",
"history_whole_open": 200,
"history_whole_closed": 150,
"history_whole_commented": 100,
"avg_days_to_resolution": 3.2,
"resolution_rate": 75.0,
"user_response_time": 1.8
}
]
}Response (Countries Comparison):
{
"type": "countries",
"entities": [
{
"country_id": 42,
"country_name": "Colombia",
"country_name_en": "Colombia",
"country_name_es": "Colombia",
"iso_alpha2": "CO",
"history_whole_open": 1000,
"history_whole_closed": 800,
"avg_days_to_resolution": 7.2,
"resolution_rate": 80.0,
"notes_health_score": 75.5,
"new_vs_resolved_ratio": 1.2,
"notes_backlog_size": 50,
"notes_created_last_30_days": 100,
"notes_resolved_last_30_days": 80
},
{
"country_id": 43,
"country_name": "Spain",
"country_name_en": "Spain",
"country_name_es": "España",
"iso_alpha2": "ES",
"history_whole_open": 2000,
"history_whole_closed": 1800,
"avg_days_to_resolution": 5.1,
"resolution_rate": 90.0,
"notes_health_score": 85.0,
"new_vs_resolved_ratio": 1.1,
"notes_backlog_size": 30,
"notes_created_last_30_days": 200,
"notes_resolved_last_30_days": 180
}
]
}Response Details:
type: Type of comparison (usersorcountries)entities: Array of entities with their metrics for side-by-side comparison
User Metrics Included:
user_id: OSM user IDusername: OSM username (may be null)history_whole_open: Total notes openedhistory_whole_closed: Total notes closedhistory_whole_commented: Total comments madeavg_days_to_resolution: Average days to resolve notesresolution_rate: Resolution rate percentageuser_response_time: Average response time in days
Country Metrics Included:
country_id: Country IDcountry_name: Country name (may be null)country_name_en: Country name in English (may be null)country_name_es: Country name in Spanish (may be null)iso_alpha2: ISO 3166-1 alpha-2 code (may be null)history_whole_open: Total notes openedhistory_whole_closed: Total notes closedavg_days_to_resolution: Average days to resolve notesresolution_rate: Resolution rate percentagenotes_health_score: Health score (0-100, may be null)new_vs_resolved_ratio: Ratio of new vs resolved notes (may be null)notes_backlog_size: Current backlog size (may be null)notes_created_last_30_days: Notes created in last 30 days (may be null)notes_resolved_last_30_days: Notes resolved in last 30 days (may be null)
Error Responses:
400 Bad Request: Missing or invalid parameters (e.g., invalid type, empty ids, invalid ID format, more than 10 IDs)500 Internal Server Error: Server error
Use Cases:
- Compare performance metrics between multiple users
- Analyze differences between countries' note management
- Benchmark user or country performance
- Identify best practices by comparing top performers
Note:
- Maximum 10 IDs can be compared in a single request
- IDs can be separated by commas with optional whitespace
- Invalid IDs in the list will cause the request to fail with 400 error
- If an ID doesn't exist, it will simply not appear in the results (empty entities array)
Search for users by username or user_id.
GET /notes-api/v1/search/users?q=<query>Query Parameters:
q(string, required): Search query (username pattern or user_id)
Examples:
Search by username pattern:
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
"http://localhost:3000/notes-api/v1/search/users?q=test"Search by exact user_id:
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
"http://localhost:3000/notes-api/v1/search/users?q=12345"Response:
{
"data": [
{
"user_id": 12345,
"username": "test_user",
"history_whole_open": 100,
"history_whole_closed": 50
}
],
"count": 1
}Behavior:
- If query is numeric (e.g., "12345"), searches for exact user_id match
- If query is text (e.g., "test"), searches username with pattern matching (case-insensitive)
- Results are limited to 50 users maximum
- Returns empty array if no matches found
Error Responses:
400 Bad Request: Missing or empty query parameter500 Internal Server Error: Server error
Search for countries by name (any language), ISO code, or country_id.
GET /notes-api/v1/search/countries?q=<query>Query Parameters:
q(string, required): Search query (country name pattern, ISO code, or country_id)
Examples:
Search by country name:
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
"http://localhost:3000/notes-api/v1/search/countries?q=Colombia"Search by ISO code:
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
"http://localhost:3000/notes-api/v1/search/countries?q=CO"Search by exact country_id:
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
"http://localhost:3000/notes-api/v1/search/countries?q=42"Response:
{
"data": [
{
"country_id": 42,
"country_name": "Colombia",
"country_name_en": "Colombia",
"country_name_es": "Colombia",
"iso_alpha2": "CO",
"history_whole_open": 1000,
"history_whole_closed": 800
}
],
"count": 1
}Behavior:
- If query is numeric (e.g., "42"), searches for exact country_id match
- If query is text, searches in
country_name,country_name_en,country_name_es, andiso_alpha2fields (case-insensitive) - Results are limited to 50 countries maximum
- Returns empty array if no matches found
Error Responses:
400 Bad Request: Missing or empty query parameter500 Internal Server Error: Server error
Get rankings of users by various metrics.
GET /notes-api/v1/users/rankings?metric=<metric>&country=<country_id>&limit=<limit>&order=<order>Query Parameters:
metric(string, required): Metric to rank by. Valid values:history_whole_open: Total notes openedhistory_whole_closed: Total notes closedhistory_whole_commented: Total comments maderesolution_rate: Resolution rate percentageavg_days_to_resolution: Average days to resolve notes
country(integer, optional): Filter rankings by country IDlimit(integer, optional): Number of results to return (1-100, default: 10)order(string, optional): Sort order -ascordesc(default:desc)
Examples:
Get top 10 users by notes opened:
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
"http://localhost:3000/notes-api/v1/users/rankings?metric=history_whole_open&limit=10"Get top 5 users by resolution rate in a specific country:
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
"http://localhost:3000/notes-api/v1/users/rankings?metric=resolution_rate&country=42&limit=5"Get users with lowest average resolution time (ascending order):
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
"http://localhost:3000/notes-api/v1/users/rankings?metric=avg_days_to_resolution&order=asc&limit=20"Response:
{
"metric": "history_whole_open",
"country": 42,
"order": "desc",
"rankings": [
{
"rank": 1,
"user_id": 12345,
"username": "top_user",
"value": 500
},
{
"rank": 2,
"user_id": 67890,
"username": "second_user",
"value": 450
}
]
}Response Fields:
metric: The metric used for rankingcountry: Country ID if filtering by country (optional)order: Sort order applied (ascordesc)rankings: Array of ranking entries, each containing:rank: Position in the ranking (1-based)user_id: User IDusername: Username (may be null)value: Metric value (may be null)
Error Responses:
400 Bad Request: Missing or invalid metric, invalid limit/order/country parameters500 Internal Server Error: Server error
Get rankings of countries by various metrics.
GET /notes-api/v1/countries/rankings?metric=<metric>&limit=<limit>&order=<order>Query Parameters:
metric(string, required): Metric to rank by. Valid values:history_whole_open: Total notes openedhistory_whole_closed: Total notes closedresolution_rate: Resolution rate percentageavg_days_to_resolution: Average days to resolve notesnotes_health_score: Overall health score for notes
limit(integer, optional): Number of results to return (1-100, default: 10)order(string, optional): Sort order -ascordesc(default:desc)
Examples:
Get top 10 countries by notes opened:
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
"http://localhost:3000/notes-api/v1/countries/rankings?metric=history_whole_open&limit=10"Get top 5 countries by resolution rate:
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
"http://localhost:3000/notes-api/v1/countries/rankings?metric=resolution_rate&limit=5"Get countries with best health scores:
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
"http://localhost:3000/notes-api/v1/countries/rankings?metric=notes_health_score&order=desc&limit=20"Response:
{
"metric": "history_whole_open",
"order": "desc",
"rankings": [
{
"rank": 1,
"country_id": 42,
"country_name": "Colombia",
"value": 100000
},
{
"rank": 2,
"country_id": 1,
"country_name": "United States",
"value": 95000
}
]
}Response Fields:
metric: The metric used for rankingorder: Sort order applied (ascordesc)rankings: Array of ranking entries, each containing:rank: Position in the ranking (1-based)country_id: Country IDcountry_name: Country name (may be null)value: Metric value (may be null)
Error Responses:
400 Bad Request: Missing or invalid metric, invalid limit/order parameters500 Internal Server Error: Server error
Get a list of all hashtags with usage counts.
GET /notes-api/v1/hashtags?page=<page>&limit=<limit>&order=<order>Query Parameters:
page(integer, optional): Page number (default: 1, minimum: 1)limit(integer, optional): Results per page (default: 50, minimum: 1, maximum: 100)order(string, optional): Sort order by count -ascordesc(default:desc)
Examples:
Get first page of hashtags (most common first):
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
"http://localhost:3000/notes-api/v1/hashtags"Get second page with 20 results per page:
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
"http://localhost:3000/notes-api/v1/hashtags?page=2&limit=20"Get hashtags sorted by count ascending (least common first):
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
"http://localhost:3000/notes-api/v1/hashtags?order=asc"Response:
{
"data": [
{
"hashtag": "fixme",
"count": 1250
},
{
"hashtag": "vandalism",
"count": 850
},
{
"hashtag": "damaged",
"count": 420
}
],
"pagination": {
"page": 1,
"limit": 50,
"total": 150,
"total_pages": 3
}
}Response Headers:
X-Total-Count: Total number of hashtagsX-Page: Current page numberX-Per-Page: Results per pageX-Total-Pages: Total number of pagesLink: Navigation links (RFC 5988)
Error Responses:
400 Bad Request: Invalid parameters (e.g., invalid order value)500 Internal Server Error: Server error
Get detailed information about a specific hashtag, including users and countries using it.
GET /notes-api/v1/hashtags/:hashtagPath Parameters:
hashtag(string, required): The hashtag name (without #). The API automatically removes # if present.
Examples:
Get details for "fixme" hashtag:
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
"http://localhost:3000/notes-api/v1/hashtags/fixme"Get details with # prefix (automatically removed):
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
"http://localhost:3000/notes-api/v1/hashtags/%23fixme"Response:
{
"hashtag": "fixme",
"users_count": 150,
"countries_count": 25,
"users": [
{
"user_id": 12345,
"username": "example_user",
"history_whole_open": 100,
"history_whole_closed": 50
},
{
"user_id": 67890,
"username": "another_user",
"history_whole_open": 80,
"history_whole_closed": 40
}
],
"countries": [
{
"country_id": 42,
"country_name": "Colombia",
"history_whole_open": 500,
"history_whole_closed": 300
},
{
"country_id": 43,
"country_name": "Spain",
"history_whole_open": 300,
"history_whole_closed": 200
}
]
}Response Details:
hashtag: The hashtag name (without #)users_count: Total number of users using this hashtagcountries_count: Total number of countries where this hashtag is usedusers: Array of up to 50 users using this hashtag, sorted byhistory_whole_open(descending)countries: Array of up to 50 countries using this hashtag, sorted byhistory_whole_open(descending)
Note: The users and countries arrays are limited to 50 entries each, but users_count and countries_count reflect the total count.
Error Responses:
400 Bad Request: Invalid hashtag parameter (empty or invalid)500 Internal Server Error: Server error
Use Cases:
- Discover popular hashtags in the OSM Notes community
- Analyze hashtag usage patterns across users and countries
- Find users or countries associated with specific topics (e.g., "#fixme", "#vandalism")
- Track hashtag trends over time
The API uses Redis-based response caching to improve performance and reduce database load. Caching is automatically enabled for GET requests to certain endpoints.
Cache Headers:
X-Cache: Indicates cache status:HIT: Response was served from cacheMISS: Response was generated and cachedDISABLED: Cache is not available (Redis not configured or error occurred)
Cached Endpoints:
/notes-api/v1/analytics/global- 10 minutes TTL/notes-api/v1/users/:user_id- 5 minutes TTL/notes-api/v1/countries/:country_id- 5 minutes TTL/notes-api/v1/users/rankings- 5 minutes TTL/notes-api/v1/countries/rankings- 5 minutes TTL
Cache Behavior:
- Only successful responses (2xx status codes) are cached
- Cache keys are generated from the request method, path, and query parameters
- Different query parameters result in different cache entries
- Cache automatically expires after the TTL (Time To Live)
- Cache gracefully degrades if Redis is unavailable (continues without caching)
Example:
# First request - Cache MISS
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
http://localhost:3000/notes-api/v1/analytics/global
# Response includes:
# X-Cache: MISS
# Second identical request - Cache HIT (if within TTL)
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
http://localhost:3000/notes-api/v1/analytics/global
# Response includes:
# X-Cache: HITNote: Cache is optional. If Redis is not configured, the API continues to work normally without caching. The X-Cache header will show DISABLED in this case.
The API exposes Prometheus metrics for monitoring and observability. The metrics endpoint is available at /metrics and does not require User-Agent validation.
Endpoint:
GET /metricsResponse Format: Prometheus text format (text/plain)
Available Metrics:
-
HTTP Request Duration (
http_request_duration_seconds):- Histogram tracking response time in seconds
- Labels:
method,route,status_code - Buckets: 0.1s, 0.5s, 1s, 2s, 5s, 10s
-
HTTP Request Count (
http_requests_total):- Counter tracking total number of HTTP requests
- Labels:
method,route,status_code
-
HTTP Error Count (
http_errors_total):- Counter tracking HTTP errors (4xx and 5xx)
- Labels:
method,route,status_code
-
Default Node.js Metrics:
- CPU usage
- Memory usage
- Event loop lag
- Active handles/requests
Example:
# Get metrics
curl http://localhost:3000/metrics
# Response (Prometheus format):
# # HELP http_requests_total Total number of HTTP requests
# # TYPE http_requests_total counter
# http_requests_total{method="GET",route="/notes-api/v1/users/:id",status_code="200"} 42
# http_request_duration_seconds_bucket{method="GET",route="/notes-api/v1/users/:id",le="0.1"} 35
# ...Integration with Prometheus:
Configure Prometheus to scrape metrics from the API:
# prometheus.yml
scrape_configs:
- job_name: 'osm-notes-api'
metrics_path: '/metrics'
static_configs:
- targets: ['api:3000']Note: The /metrics endpoint is excluded from User-Agent validation and rate limiting to allow Prometheus to scrape metrics without restrictions.
200 OK: Successful request400 Bad Request: Invalid request (missing User-Agent, invalid parameters)403 Forbidden: Access denied (AI without OAuth, blocked bot)404 Not Found: Resource not found429 Too Many Requests: Rate limit exceeded500 Internal Server Error: Server error
{
"error": "Error type",
"message": "Human-readable error message",
"details": {
"field": "Additional error details"
}
}Example:
{
"error": "Invalid User-Agent",
"message": "User-Agent must follow format: AppName/Version (Contact)",
"details": {
"format": "AppName/Version (Contact)",
"received": "MyApp/1.0"
}
}# ✅ Correct
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
http://localhost:3000/notes-api/v1/users/12345
# ❌ Incorrect
curl http://localhost:3000/notes-api/v1/users/12345- Implement retry with exponential backoff
- Respect
RateLimit-*headers (standard headers, notX-RateLimit-*) - Use
RateLimit-Resetto know when to retry
try {
const response = await fetch(url, {
headers: { 'User-Agent': 'MyApp/1.0 (contact@example.com)' }
});
if (response.status === 429) {
const resetTime = response.headers.get('RateLimit-Reset');
// Wait until resetTime
}
const data = await response.json();
} catch (error) {
// Handle error
}For endpoints that return lists, use pagination:
GET /notes-api/v1/notes?page=1&limit=20Pagination Query Parameters:
page(number): Page number (default: 1, minimum: 1)limit(number): Results per page (default: 20, maximum: 100, minimum: 1)
Pagination Response Body: All paginated endpoints include pagination metadata in the response body:
{
"data": [...],
"pagination": {
"page": 1,
"limit": 20,
"total": 250,
"total_pages": 13
}
}Pagination HTTP Headers: The API also includes standard pagination headers in HTTP responses:
X-Total-Count: Total number of resultsX-Page: Current page numberX-Per-Page: Number of results per pageX-Total-Pages: Total number of pagesLink: Navigation links (RFC 5988) withrelvalues:first: Link to first pageprev: Link to previous page (if not on first page)next: Link to next page (if not on last page)last: Link to last page
Example Response Headers:
X-Total-Count: 250
X-Page: 2
X-Per-Page: 20
X-Total-Pages: 13
Link: </notes-api/v1/notes?page=1&limit=20>; rel="first", </notes-api/v1/notes?page=1&limit=20>; rel="prev", </notes-api/v1/notes?page=3&limit=20>; rel="next", </notes-api/v1/notes?page=13&limit=20>; rel="last"
Using Pagination Headers: You can use these headers to implement pagination navigation without parsing the response body:
const response = await fetch('/notes-api/v1/notes?page=2&limit=20', {
headers: { 'User-Agent': 'MyApp/1.0 (contact@example.com)' }
});
const totalPages = parseInt(response.headers.get('X-Total-Pages'), 10);
const currentPage = parseInt(response.headers.get('X-Page'), 10);
const linkHeader = response.headers.get('Link');
// Parse Link header to get navigation URLs
const links = parseLinkHeader(linkHeader);
// links.first, links.prev, links.next, links.lastNote: Query parameters are preserved in pagination Link headers, so filters are maintained when navigating between pages.
Responses include cache headers when applicable. Respect Cache-Control and ETag.
For complete documentation of all endpoints:
- Swagger UI:
http://localhost:3000/docs- Interactive API documentation - OpenAPI Spec (JSON):
http://localhost:3000/docs/json- OpenAPI specification in JSON format - Documentation: See docs/api/ for complete specifications
The API includes interactive Swagger documentation that allows you to:
- Browse all available endpoints
- See request/response schemas
- Test endpoints directly from the browser
- View example requests and responses
Access: Navigate to http://localhost:3000/docs in your browser when the server is running.
Note: Swagger UI is excluded from User-Agent validation for easier access, but all API endpoints still require the User-Agent header.
If you have questions about using the API:
- Review the complete documentation
- Check the examples in this manual
- Open an issue on GitHub if you encounter problems