Read-only access to your climbing data for third-party apps and scripts
Two schemes are used depending on the endpoint.
All requests must include the user's API key in the X-API-Key header:
X-API-Key: cnl_your_key_here
Users generate keys in the SendLog Web App under My Account → API Keys.
Each API key is limited to 1 000 requests per hour. The window resets on a rolling basis per key.
When the limit is exceeded:
HTTP 429 Too Many Requests
{ "error": "Rate limit exceeded (1000 req/hour)" }
List endpoints that can return large result sets use cursor-based pagination.
| Parameter | Type | Description |
|---|---|---|
limit |
integer | Results per page. Min 1, max 100, default 50. |
after |
string | Opaque cursor returned as nextCursor in the previous response. |
Response envelope for paginated endpoints:
{
"data": [ ... ],
"nextCursor": "eyJpZCI6Ii4uLiJ9",
"hasMore": true
}
When hasMore is false, nextCursor is null — you have reached the last page.
All errors return a JSON body with a human-readable error field:
{ "error": "Required scopes: climbs:read" }
| Status | Meaning |
|---|---|
400 | Bad request — invalid query parameter (e.g. non-ISO 8601 date) |
401 | Missing or invalid credentials |
403 | Valid key but missing a required scope |
404 | Resource not found or deleted |
429 | Rate limit exceeded |
500 | Internal server error |
All data endpoints authenticate with X-API-Key and return data belonging to the user who generated the key.
List climb notes (sends and projects). Ordered by date ascending by default, or by updatedAt ascending when since is used.
| Parameter | Type | Description |
|---|---|---|
limit | integer | Results per page (1–100, default 50) |
after | string | Pagination cursor from nextCursor |
from | ISO 8601 date | Climbs on or after this date |
to | ISO 8601 date | Climbs on or before this date |
since | ISO 8601 datetime | Only records updated after this timestamp. Mutually exclusive with from/to. |
include | string | Comma-separated list of optional fields. Currently supported: gps. |
curl "https://api-hoxktcdqvq-uc.a.run.app/v1/climbs?from=2026-01-01&limit=20" \
-H "X-API-Key: cnl_your_key_here"
200 OK{
"data": [
{
"id": "abc123",
"route": "Biographie",
"climbingArea": "Céüse",
"crag": "La Face",
"difficulty": "9a",
"sendType": "Redpoint",
"routeType": "Sport",
"noteText": "Dream route, finally!",
"rating": 5,
"attemptCount": 47,
"date": "2026-08-15T00:00:00.000Z",
"lastAttemptDate": null,
"projectStatus": null,
"projectNotes": null,
"highPoint": null,
"centralRouteID": "rTeho8rot8CTj9kEvZE4",
"updatedAt": "2026-08-15T18:30:00.000Z"
}
],
"nextCursor": null,
"hasMore": false
}
| Field | Type | Description |
|---|---|---|
id | string | Firestore document ID |
route | string | Route name |
climbingArea | string | Broader area / region |
crag | string | null | Specific crag or wall |
difficulty | string | Grade (French, UIAA, or YDS depending on user preference) |
sendType | string | Redpoint · Pinkpoint · On Sight · Flash · Top Rope · All Free · Project |
routeType | string | Sport · Boulder · Multi-Pitch |
noteText | string | null | Free-text notes |
rating | integer | 0–5 stars |
attemptCount | integer | Number of attempts logged |
date | ISO 8601 | null | Send date (null for active projects) |
lastAttemptDate | ISO 8601 | null | Last attempt date (projects) |
projectStatus | string | null | Working · Close · On Hold |
projectNotes | string | null | Project-specific notes |
highPoint | string | null | Highest point reached (projects) |
centralRouteID | string | null | ID of the linked entry in the SendLog central route database, or null if not linked |
updatedAt | ISO 8601 | null | Last modification timestamp |
?include=gps)Add ?include=gps to include GPS coordinates sourced from the SendLog central route database. When requested, each climb object gains a gps field:
gps is a coordinates object if the linked central route has GPS stored.gps is null if the climb has no centralRouteID, or if the central route does not yet have GPS coordinates.?include=gps the gps key is absent entirely — existing integrations are unaffected.curl "https://api-hoxktcdqvq-uc.a.run.app/v1/climbs?include=gps" \
-H "X-API-Key: cnl_your_key_here"
{
"data": [
{
"id": "abc123",
"route": "Biographie",
"centralRouteID": "rTeho8rot8CTj9kEvZE4",
"gps": { "latitude": 44.1823, "longitude": 5.9714, "country": "FR" },
...
},
{
"id": "def456",
"route": "My Local Project",
"centralRouteID": null,
"gps": null,
...
}
]
}
| GPS field | Type | Description |
|---|---|---|
latitude | number | WGS84 latitude |
longitude | number | WGS84 longitude |
country | string | null | ISO 3166-1 alpha-2 country code (e.g. "AT", "FR") |
Get a single climb note with its full repeat-ascent history. Supports the same ?include=gps option as the list endpoint.
| Parameter | Type | Description |
|---|---|---|
include | string | Optional fields. gps fetches GPS coordinates from the central route database. |
curl "https://api-hoxktcdqvq-uc.a.run.app/v1/climbs/abc123?include=gps" \
-H "X-API-Key: cnl_your_key_here"
200 OKSame fields as the list item, plus an ascents array (and gps if requested):
{
"id": "abc123",
"route": "Biographie",
"centralRouteID": "rTeho8rot8CTj9kEvZE4",
"gps": { "latitude": 44.1823, "longitude": 5.9714, "country": "FR" },
"ascents": [
{
"id": "asc-uuid-1",
"sendType": "Redpoint",
"date": "2026-09-10T00:00:00.000Z",
"notes": "Even better second time"
}
]
}
| Field | Type | Description |
|---|---|---|
id | string | Ascent UUID |
sendType | string | Send type for this repeat ascent |
date | ISO 8601 | null | Date of the repeat ascent |
notes | string | null | Notes specific to this ascent |
Returns 404 if the climb does not exist or has been deleted.
List training sessions, ordered by date ascending (or updatedAt when since is used).
| Parameter | Type | Description |
|---|---|---|
limit | integer | Results per page (1–100, default 50) |
after | string | Pagination cursor |
from | ISO 8601 date | Sessions on or after this date |
to | ISO 8601 date | Sessions on or before this date |
since | ISO 8601 datetime | Incremental sync — records updated after this timestamp |
curl "https://api-hoxktcdqvq-uc.a.run.app/v1/training?from=2026-01-01" \
-H "X-API-Key: cnl_your_key_here"
200 OK{
"data": [
{
"id": "ts-uuid-1",
"type": "Hangboard",
"duration": 45,
"intensity": 4,
"notes": "Max hangs protocol, 20mm edge.",
"date": "2026-03-20T00:00:00.000Z",
"updatedAt": "2026-03-20T19:00:00.000Z"
}
],
"nextCursor": null,
"hasMore": false
}
| Field | Type | Description |
|---|---|---|
id | string | Session UUID |
type | string | Hangboard · Campus Board · Gym Session · Running · Yoga · or custom |
duration | integer | Duration in minutes |
intensity | integer | 1–5 intensity rating |
notes | string | null | Free-text notes |
date | ISO 8601 | null | Session date |
updatedAt | ISO 8601 | null | Last modification timestamp |
List goals. Supports since for incremental sync. No pagination (goal lists are typically small).
| Parameter | Type | Description |
|---|---|---|
since | ISO 8601 datetime | Only goals updated after this timestamp |
curl "https://api-hoxktcdqvq-uc.a.run.app/v1/goals" \
-H "X-API-Key: cnl_your_key_here"
200 OK{
"data": [
{
"id": "goal-uuid-1",
"type": "gradeTarget",
"target": 0,
"periodIsWeekly": false,
"gradeTarget": "8a",
"deadline": "2026-12-31T00:00:00.000Z",
"achievedAt": null,
"isArchived": false,
"completedPeriods": [],
"updatedAt": "2026-04-01T10:00:00.000Z"
}
]
}
| Field | Type | Description |
|---|---|---|
id | string | Goal UUID |
type | string | gradeTarget · climbingDays · trainingDays |
target | integer | Numeric target (e.g. days per period) |
periodIsWeekly | boolean | true = weekly, false = monthly |
gradeTarget | string | null | Target grade (grade-type goals only) |
deadline | ISO 8601 | null | Optional deadline |
achievedAt | ISO 8601 | null | When the goal was first achieved |
isArchived | boolean | Whether the goal has been archived |
completedPeriods | string[] | ISO week/month strings for completed periods |
updatedAt | ISO 8601 | null | Last modification timestamp |
All list endpoints support a since parameter for efficient incremental sync. Pass the updatedAt of the most recently seen record to fetch only records changed after that point. When using since, results are ordered by updatedAt ascending and the parameter is mutually exclusive with from/to.
# 1. Initial full fetch — store the max updatedAt from the response
curl "https://api-hoxktcdqvq-uc.a.run.app/v1/climbs?limit=100" \
-H "X-API-Key: cnl_your_key_here"
# 2. On subsequent runs, pass it as `since`
curl "https://api-hoxktcdqvq-uc.a.run.app/v1/climbs?since=2026-04-30T12:00:00.000Z" \
-H "X-API-Key: cnl_your_key_here"
since — a large batch update could produce more than 100 changed records at once.
API_KEY="cnl_your_key_here"
BASE="https://api-hoxktcdqvq-uc.a.run.app"
curl "$BASE/v1/climbs?from=2026-01-01&limit=100" \
-H "X-API-Key: $API_KEY"
import requests
API_KEY = "cnl_your_key_here"
BASE = "https://api-hoxktcdqvq-uc.a.run.app"
HEADERS = {"X-API-Key": API_KEY}
# Fetch all climbs (handles pagination automatically)
climbs, cursor = [], None
while True:
params = {"limit": 100}
if cursor:
params["after"] = cursor
r = requests.get(f"{BASE}/v1/climbs", headers=HEADERS, params=params)
r.raise_for_status()
body = r.json()
climbs.extend(body["data"])
cursor = body.get("nextCursor")
if not body.get("hasMore"):
break
sends = [c for c in climbs if c["sendType"] != "Project"]
print(f"Total sends : {len(sends)}")
print(f"Hardest grade: {max((c['difficulty'] for c in sends), default='—')}")
Last Updated: April 2026 · API v1